diff options
19 files changed, 1331 insertions, 1065 deletions
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index 6e1f8b535bff..18a59d863c46 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -20,12 +20,15 @@ import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; import static com.android.internal.util.CollectionUtils.emptyIfNull; +import static java.util.Objects.requireNonNull; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; import android.annotation.SystemApi; +import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Parcel; @@ -52,12 +55,11 @@ import java.util.Objects; * device to be shown instead of a list to choose from */ @DataClass( + genConstructor = false, genToString = true, genEqualsHashCode = true, genHiddenGetters = true, genParcelable = true, - genHiddenConstructor = true, - genBuilder = false, genConstDefs = false) public final class AssociationRequest implements Parcelable { /** @@ -151,40 +153,76 @@ public final class AssociationRequest implements Parcelable { private final boolean mForceConfirmation; /** - * The app package making the request. - * + * The app package name of the application the association will belong to. + * Populated by the system. + * @hide + */ + private @Nullable String mPackageName; + + /** + * The UserId of the user the association will belong to. * Populated by the system. - * * @hide */ - private @Nullable String mCallingPackage; + private @UserIdInt int mUserId; /** * The user-readable description of the device profile's privileges. - * * Populated by the system. - * * @hide */ private @Nullable String mDeviceProfilePrivilegesDescription; /** * The time at which his request was created - * * @hide */ - private long mCreationTime; + private final long mCreationTime; /** * Whether the user-prompt may be skipped once the device is found. - * * Populated by the system. - * * @hide */ private boolean mSkipPrompt; /** + * Creates a new AssociationRequest. + * + * @param singleDevice + * Whether only a single device should match the provided filter. + * + * When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac + * address, bonded devices are also searched among. This allows to obtain the necessary app + * privileges even if the device is already paired. + * @param deviceFilters + * If set, only devices matching either of the given filters will be shown to the user + * @param deviceProfile + * Profile of the device. + * @param displayName + * The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for + * "self-managed" association. + * @param selfManaged + * Whether the association is to be managed by the companion application. + */ + private AssociationRequest( + boolean singleDevice, + @NonNull List<DeviceFilter<?>> deviceFilters, + @Nullable @DeviceProfile String deviceProfile, + @Nullable CharSequence displayName, + boolean selfManaged, + boolean forceConfirmation) { + mSingleDevice = singleDevice; + mDeviceFilters = requireNonNull(deviceFilters); + mDeviceProfile = deviceProfile; + mDisplayName = displayName; + mSelfManaged = selfManaged; + mForceConfirmation = forceConfirmation; + + mCreationTime = System.currentTimeMillis(); + } + + /** * @return profile of the companion device. */ public @Nullable @DeviceProfile String getDeviceProfile() { @@ -237,8 +275,13 @@ public final class AssociationRequest implements Parcelable { } /** @hide */ - public void setCallingPackage(@NonNull String pkg) { - mCallingPackage = pkg; + public void setPackageName(@NonNull String packageName) { + mPackageName = packageName; + } + + /** @hide */ + public void setUserId(@UserIdInt int userId) { + mUserId = userId; } /** @hide */ @@ -248,7 +291,7 @@ public final class AssociationRequest implements Parcelable { /** @hide */ public void setSkipPrompt(boolean value) { - mSkipPrompt = true; + mSkipPrompt = value; } /** @hide */ @@ -258,10 +301,6 @@ public final class AssociationRequest implements Parcelable { return mDeviceFilters; } - private void onConstructed() { - mCreationTime = System.currentTimeMillis(); - } - /** * A builder for {@link AssociationRequest} */ @@ -325,7 +364,7 @@ public final class AssociationRequest implements Parcelable { @NonNull public Builder setDisplayName(@NonNull CharSequence displayName) { checkNotUsed(); - mDisplayName = Objects.requireNonNull(displayName); + mDisplayName = requireNonNull(displayName); return this; } @@ -372,15 +411,13 @@ public final class AssociationRequest implements Parcelable { + "provide the display name of the device"); } return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters), - mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation, - null, null, -1L, false); + mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation); } } - // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! @@ -395,88 +432,29 @@ public final class AssociationRequest implements Parcelable { /** - * Creates a new AssociationRequest. - * - * @param singleDevice - * Whether only a single device should match the provided filter. - * - * When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac - * address, bonded devices are also searched among. This allows to obtain the necessary app - * privileges even if the device is already paired. - * @param deviceFilters - * If set, only devices matching either of the given filters will be shown to the user - * @param deviceProfile - * Profile of the device. - * @param displayName - * The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for - * "self-managed" association. - * @param selfManaged - * Whether the association is to be managed by the companion application. - * @param forceConfirmation - * Indicates that the application would prefer the CompanionDeviceManager to collect an explicit - * confirmation from the user before creating an association, even if such confirmation is not - * required. - * @param callingPackage - * The app package making the request. - * - * Populated by the system. - * @param deviceProfilePrivilegesDescription - * The user-readable description of the device profile's privileges. - * - * Populated by the system. - * @param creationTime - * The time at which his request was created - * @param skipPrompt - * Whether the user-prompt may be skipped once the device is found. + * The app package name of the application the association will belong to. + * Populated by the system. * - * Populated by the system. * @hide */ @DataClass.Generated.Member - public AssociationRequest( - boolean singleDevice, - @NonNull List<DeviceFilter<?>> deviceFilters, - @Nullable @DeviceProfile String deviceProfile, - @Nullable CharSequence displayName, - boolean selfManaged, - boolean forceConfirmation, - @Nullable String callingPackage, - @Nullable String deviceProfilePrivilegesDescription, - long creationTime, - boolean skipPrompt) { - this.mSingleDevice = singleDevice; - this.mDeviceFilters = deviceFilters; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mDeviceFilters); - this.mDeviceProfile = deviceProfile; - com.android.internal.util.AnnotationValidations.validate( - DeviceProfile.class, null, mDeviceProfile); - this.mDisplayName = displayName; - this.mSelfManaged = selfManaged; - this.mForceConfirmation = forceConfirmation; - this.mCallingPackage = callingPackage; - this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription; - this.mCreationTime = creationTime; - this.mSkipPrompt = skipPrompt; - - onConstructed(); + public @Nullable String getPackageName() { + return mPackageName; } /** - * The app package making the request. - * + * The UserId of the user the association will belong to. * Populated by the system. * * @hide */ @DataClass.Generated.Member - public @Nullable String getCallingPackage() { - return mCallingPackage; + public @UserIdInt int getUserId() { + return mUserId; } /** * The user-readable description of the device profile's privileges. - * * Populated by the system. * * @hide @@ -498,7 +476,6 @@ public final class AssociationRequest implements Parcelable { /** * Whether the user-prompt may be skipped once the device is found. - * * Populated by the system. * * @hide @@ -521,7 +498,8 @@ public final class AssociationRequest implements Parcelable { "displayName = " + mDisplayName + ", " + "selfManaged = " + mSelfManaged + ", " + "forceConfirmation = " + mForceConfirmation + ", " + - "callingPackage = " + mCallingPackage + ", " + + "packageName = " + mPackageName + ", " + + "userId = " + mUserId + ", " + "deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " + "creationTime = " + mCreationTime + ", " + "skipPrompt = " + mSkipPrompt + @@ -547,7 +525,8 @@ public final class AssociationRequest implements Parcelable { && Objects.equals(mDisplayName, that.mDisplayName) && mSelfManaged == that.mSelfManaged && mForceConfirmation == that.mForceConfirmation - && Objects.equals(mCallingPackage, that.mCallingPackage) + && Objects.equals(mPackageName, that.mPackageName) + && mUserId == that.mUserId && Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription) && mCreationTime == that.mCreationTime && mSkipPrompt == that.mSkipPrompt; @@ -566,7 +545,8 @@ public final class AssociationRequest implements Parcelable { _hash = 31 * _hash + Objects.hashCode(mDisplayName); _hash = 31 * _hash + Boolean.hashCode(mSelfManaged); _hash = 31 * _hash + Boolean.hashCode(mForceConfirmation); - _hash = 31 * _hash + Objects.hashCode(mCallingPackage); + _hash = 31 * _hash + Objects.hashCode(mPackageName); + _hash = 31 * _hash + mUserId; _hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription); _hash = 31 * _hash + Long.hashCode(mCreationTime); _hash = 31 * _hash + Boolean.hashCode(mSkipPrompt); @@ -583,16 +563,17 @@ public final class AssociationRequest implements Parcelable { if (mSingleDevice) flg |= 0x1; if (mSelfManaged) flg |= 0x10; if (mForceConfirmation) flg |= 0x20; - if (mSkipPrompt) flg |= 0x200; + if (mSkipPrompt) flg |= 0x400; if (mDeviceProfile != null) flg |= 0x4; if (mDisplayName != null) flg |= 0x8; - if (mCallingPackage != null) flg |= 0x40; - if (mDeviceProfilePrivilegesDescription != null) flg |= 0x80; + if (mPackageName != null) flg |= 0x40; + if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100; dest.writeInt(flg); dest.writeParcelableList(mDeviceFilters, flags); if (mDeviceProfile != null) dest.writeString(mDeviceProfile); if (mDisplayName != null) dest.writeCharSequence(mDisplayName); - if (mCallingPackage != null) dest.writeString(mCallingPackage); + if (mPackageName != null) dest.writeString(mPackageName); + dest.writeInt(mUserId); if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription); dest.writeLong(mCreationTime); } @@ -612,13 +593,14 @@ public final class AssociationRequest implements Parcelable { boolean singleDevice = (flg & 0x1) != 0; boolean selfManaged = (flg & 0x10) != 0; boolean forceConfirmation = (flg & 0x20) != 0; - boolean skipPrompt = (flg & 0x200) != 0; + boolean skipPrompt = (flg & 0x400) != 0; List<DeviceFilter<?>> deviceFilters = new ArrayList<>(); in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader()); String deviceProfile = (flg & 0x4) == 0 ? null : in.readString(); CharSequence displayName = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence(); - String callingPackage = (flg & 0x40) == 0 ? null : in.readString(); - String deviceProfilePrivilegesDescription = (flg & 0x80) == 0 ? null : in.readString(); + String packageName = (flg & 0x40) == 0 ? null : in.readString(); + int userId = in.readInt(); + String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString(); long creationTime = in.readLong(); this.mSingleDevice = singleDevice; @@ -631,12 +613,15 @@ public final class AssociationRequest implements Parcelable { this.mDisplayName = displayName; this.mSelfManaged = selfManaged; this.mForceConfirmation = forceConfirmation; - this.mCallingPackage = callingPackage; + this.mPackageName = packageName; + this.mUserId = userId; + com.android.internal.util.AnnotationValidations.validate( + UserIdInt.class, null, mUserId); this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription; this.mCreationTime = creationTime; this.mSkipPrompt = skipPrompt; - onConstructed(); + // onConstructed(); // You can define this method to get a callback } @DataClass.Generated.Member @@ -654,10 +639,10 @@ public final class AssociationRequest implements Parcelable { }; @DataClass.Generated( - time = 1638368698639L, + time = 1638962248060L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java", - inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final boolean mSelfManaged\nprivate final boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate long mCreationTime\nprivate boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic boolean isSingleDevice()\npublic void setCallingPackage(java.lang.String)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nprivate void onConstructed()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate boolean mSelfManaged\nprivate boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false, genConstDefs=false)") + inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final boolean mSelfManaged\nprivate final boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mPackageName\nprivate @android.annotation.UserIdInt int mUserId\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate final long mCreationTime\nprivate boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic boolean isSingleDevice()\npublic void setPackageName(java.lang.String)\npublic void setUserId(int)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate boolean mSelfManaged\nprivate boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml index c5926a5c75cc..06f2d9d0f0c8 100644 --- a/packages/CompanionDeviceManager/AndroidManifest.xml +++ b/packages/CompanionDeviceManager/AndroidManifest.xml @@ -19,10 +19,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.companiondevicemanager"> - <permission - android:name="com.android.companiondevicemanager.permission.BIND" - android:protectionLevel="signature" /> - <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/> @@ -43,23 +39,17 @@ android:forceQueryable="true" android:supportsRtl="true"> - <service - android:name=".CompanionDeviceDiscoveryService" - android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE" - android:exported="true"> - </service> - <activity android:name=".CompanionDeviceActivity" - android:theme="@style/ChooserActivity" + android:exported="true" + android:launchMode="singleInstance" + android:excludeFromRecents="true" android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE" - android:exported="true"> - <!--TODO include url scheme filter similar to PrintSpooler --> - <intent-filter> - <action android:name="android.companiondevice.START_DISCOVERY" /> - <category android:name="android.intent.category.DEFAULT" /> - </intent-filter> - </activity> + android:theme="@style/ChooserActivity"/> + + <service + android:name=".CompanionDeviceDiscoveryService" + android:exported="false" /> </application> diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml new file mode 100644 index 000000000000..c87bac67fdff --- /dev/null +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/dialog_background" + android:elevation="16dp" + android:maxHeight="400dp" + android:orientation="vertical" + android:padding="18dp" + android:layout_gravity="center"> + + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingHorizontal="12dp" + style="@*android:style/TextAppearance.Widget.Toolbar.Title"/> + <!-- style="@*android:style/TextAppearance.Widget.Toolbar.Title" --> + + <TextView + android:id="@+id/summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" + android:gravity="center" + android:textColor="?android:attr/textColorSecondary" + android:textSize="14sp" /> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <ListView + android:id="@+id/device_list" + style="@android:style/Widget.Material.ListView" + android:layout_width="match_parent" + android:layout_height="200dp" /> + + </RelativeLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="end"> + + <Button + android:id="@+id/button_cancel" + style="@android:style/Widget.Material.Button.Borderless.Colored" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/consent_no" + android:textColor="?android:attr/textColorSecondary" /> + + <Button + android:id="@+id/button_allow" + style="@android:style/Widget.Material.Button.Borderless.Colored" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/consent_yes" /> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/buttons.xml b/packages/CompanionDeviceManager/res/layout/buttons.xml deleted file mode 100644 index a80720c40737..000000000000 --- a/packages/CompanionDeviceManager/res/layout/buttons.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> - - -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/buttons" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_alignParentBottom="true" - android:layout_alignParentEnd="true" - android:gravity="end" -> - <Button - android:id="@+id/button_cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/consent_no" - android:textColor="?android:attr/textColorSecondary" - style="@android:style/Widget.Material.Button.Borderless.Colored" - /> - <Button - android:id="@+id/button_pair" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/consent_yes" - style="@android:style/Widget.Material.Button.Borderless.Colored" - /> -</LinearLayout>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/device_chooser.xml b/packages/CompanionDeviceManager/res/layout/device_chooser.xml deleted file mode 100644 index 273347af6119..000000000000 --- a/packages/CompanionDeviceManager/res/layout/device_chooser.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> - -<RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/container" - android:layout_height="400dp" - style="@style/ContainerLayout" - > - - <include layout="@layout/title" /> - - <include layout="@layout/profile_summary" /> - - <ListView - android:id="@+id/device_list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_below="@+id/profile_summary" - android:layout_above="@+id/buttons" - style="@android:style/Widget.Material.ListView" - /> - - <include layout="@layout/buttons" /> - -</RelativeLayout>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml b/packages/CompanionDeviceManager/res/layout/device_confirmation.xml deleted file mode 100644 index 1336e79a855b..000000000000 --- a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> - -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/container" - android:layout_height="wrap_content" - style="@style/ContainerLayout" - > - - <include layout="@layout/title" /> - - <include layout="@layout/profile_summary" /> - - <include layout="@layout/buttons" /> - -</LinearLayout>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/profile_summary.xml b/packages/CompanionDeviceManager/res/layout/profile_summary.xml deleted file mode 100644 index 80fec59fbc45..000000000000 --- a/packages/CompanionDeviceManager/res/layout/profile_summary.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2020 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. - --> - - -<TextView - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/profile_summary" - android:layout_below="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" - android:textColor="?android:attr/textColorSecondary" - android:textSize="14sp" - android:gravity="center" -/>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/title.xml b/packages/CompanionDeviceManager/res/layout/title.xml deleted file mode 100644 index 9a5036622468..000000000000 --- a/packages/CompanionDeviceManager/res/layout/title.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> - - -<TextView - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center" - style="@*android:style/TextAppearance.Widget.Toolbar.Title" -/>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/values/dimens.xml b/packages/CompanionDeviceManager/res/values/dimens.xml deleted file mode 100644 index da7b0d1447c1..000000000000 --- a/packages/CompanionDeviceManager/res/values/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <!-- Padding applied on most UI elements --> - <dimen name="padding">12dp</dimen> - -</resources>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 44748e9cc692..cb8b616ec009 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -19,25 +19,58 @@ <!-- Title of the CompanionDeviceManager application. [CHAR LIMIT=50] --> <string name="app_label">Companion Device Manager</string> - <!-- Title of the device selection dialog. --> - <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string> + <!-- Title of the device association confirmation dialog. --> + <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string> - <!-- The generic placeholder for a device type when nothing specific is known about it [CHAR LIMIT=30] --> - <string name="profile_name_generic">device</string> + <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= --> <!-- The name of the "watch" device type [CHAR LIMIT=30] --> <string name="profile_name_watch">watch</string> - <!-- Title of the device association confirmation dialog. --> - <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string> + <!-- Title of the device selection dialog. --> + <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string> + + <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string> + + <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string> + + <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> - <!-- Text of the device profile permissions explanation in the association dialog. --> - <string name="profile_summary">This app is needed to manage your <xliff:g id="profile_name" example="watch">%1$s</xliff:g>. <xliff:g id="privileges_discplaimer" example="Android Wear will get access to your Notifications, Calendar and Contacts.">%2$s</xliff:g></string> + <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> + <string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream applications?</string> + + <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_app_streaming" product="default">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this phone when connected.</string> + + <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_app_streaming" product="tablet">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this tablet when connected.</string> + + <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_app_streaming" product="device">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this device when connected.</string> + + <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= --> + + <!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] --> + <string name="title_automotive_projection"></string> + + <!-- Description of the privileges the application will get if associated with the companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_automotive_projection"></string> + + <!-- ================= null profile ================= --> + + <!-- A noun for a companion device with unspecified profile (type) [CHAR LIMIT=30] --> + <string name="profile_name_generic">device</string> + + <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] --> + <string name="summary_generic"></string> + + <!-- ================= Buttons ================= --> <!-- Positive button for the device-app association consent dialog [CHAR LIMIT=30] --> <string name="consent_yes">Allow</string> <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] --> <string name="consent_no">Don\u2019t allow</string> - </resources> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml deleted file mode 100644 index 9dced47bb36f..000000000000 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 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. ---> - -<resources> - <style name="ContainerLayout"> - <item name="android:orientation">vertical</item> - <item name="android:layout_width">match_parent</item> - <item name="android:elevation">16dp</item> - <item name="android:background">@drawable/dialog_background</item> - <item name="android:paddingTop">18dip</item> - <item name="android:paddingStart">20dip</item> - <item name="android:paddingEnd">16dip</item> - <item name="android:paddingBottom">16dip</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/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index a5168ccd977c..cc887c34414e 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -16,327 +16,356 @@ package com.android.companiondevicemanager; -import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; -import static android.text.TextUtils.emptyIfNull; -import static android.text.TextUtils.isEmpty; -import static android.text.TextUtils.withoutPrefix; +import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING; +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; +import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import static com.android.companiondevicemanager.Utils.getApplicationLabel; +import static com.android.companiondevicemanager.Utils.getHtmlFromResources; +import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc; + import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; +import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; +import android.companion.IAssociationRequestCallback; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Color; -import android.graphics.drawable.Drawable; +import android.net.MacAddress; import android.os.Bundle; -import android.text.Html; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.text.Spanned; import android.util.Log; -import android.util.SparseArray; -import android.util.TypedValue; -import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; +import android.widget.Button; import android.widget.ListView; -import android.widget.ProgressBar; import android.widget.TextView; -import com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DeviceFilterPair; -import com.android.internal.util.Preconditions; - public class CompanionDeviceActivity extends Activity { - private static final boolean DEBUG = false; - private static final String LOG_TAG = CompanionDeviceActivity.class.getSimpleName(); + private static final String TAG = CompanionDeviceActivity.class.getSimpleName(); + + // Keep the following constants in sync with + // frameworks/base/services/companion/java/ + // com/android/server/companion/AssociationRequestsProcessor.java + + // AssociationRequestsProcessor <-> UI + private static final String EXTRA_APPLICATION_CALLBACK = "application_callback"; + private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; + private static final String EXTRA_RESULT_RECEIVER = "result_receiver"; + + // AssociationRequestsProcessor -> UI + private static final int RESULT_CODE_ASSOCIATION_CREATED = 0; + private static final String EXTRA_ASSOCIATION = "association"; - static CompanionDeviceActivity sInstance; + // UI -> AssociationRequestsProcessor + private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0; + private static final String EXTRA_MAC_ADDRESS = "mac_address"; - View mLoadingIndicator = null; - ListView mDeviceListView; - private View mPairButton; - private View mCancelButton; + private AssociationRequest mRequest; + private IAssociationRequestCallback mAppCallback; + private ResultReceiver mCdmServiceReceiver; - DevicesAdapter mDevicesAdapter; + // Always present widgets. + private TextView mTitle; + private TextView mSummary; + + // Progress indicator is only shown while we are looking for the first suitable device for a + // "regular" (ie. not self-managed) association. + private View mProgressIndicator; + + // Present for self-managed association requests and "single-device" regular association + // regular. + private Button mButtonAllow; + + // The list is only shown for multiple-device regular association request, after at least one + // matching device is found. + private @Nullable ListView mListView; + private @Nullable DeviceListAdapter mAdapter; + + // The flag used to prevent double taps, that may lead to sending several requests for creating + // an association to CDM. + private boolean mAssociationApproved; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + } - Log.i(LOG_TAG, "Starting UI for " + getService().mRequest); + @Override + protected void onStart() { + super.onStart(); + if (DEBUG) Log.d(TAG, "onStart()"); + + final Intent intent = getIntent(); + mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST); + mAppCallback = IAssociationRequestCallback.Stub.asInterface( + intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); + mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); + + requireNonNull(mRequest); + requireNonNull(mAppCallback); + requireNonNull(mCdmServiceReceiver); + + // Start discovery services if needed. + if (!mRequest.isSelfManaged()) { + CompanionDeviceDiscoveryService.startForRequest(this, mRequest); + } + // Init UI. + initUI(); + } - if (getService().mDevicesFound.isEmpty()) { - Log.e(LOG_TAG, "About to show UI, but no devices to show"); + @Override + protected void onStop() { + super.onStop(); + if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing()); + + // TODO: handle config changes without cancelling. + if (!isFinishing()) { + cancel(); // will finish() } - getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - sInstance = this; - getService().mActivity = this; - - String deviceProfile = getRequest().getDeviceProfile(); - String profilePrivacyDisclaimer = emptyIfNull(getRequest() - .getDeviceProfilePrivilegesDescription()) - .replace("APP_NAME", getCallingAppName()); - boolean useDeviceProfile = deviceProfile != null && !isEmpty(profilePrivacyDisclaimer); - String profileName = useDeviceProfile - ? getDeviceProfileName(deviceProfile) - : getString(R.string.profile_name_generic); - - if (getRequest().isSingleDevice()) { - setContentView(R.layout.device_confirmation); - final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0); - setTitle(Html.fromHtml(getString( - R.string.confirmation_title, - Html.escapeHtml(getCallingAppName()), - Html.escapeHtml(selectedDevice.getDisplayName())), 0)); - - mPairButton = findViewById(R.id.button_pair); - mPairButton.setOnClickListener(v -> onDeviceConfirmed(getService().mSelectedDevice)); - getService().mSelectedDevice = selectedDevice; - onSelectionUpdate(); - if (getRequest().isSkipPrompt()) { - onDeviceConfirmed(selectedDevice); - } - } else { - setContentView(R.layout.device_chooser); - mPairButton = findViewById(R.id.button_pair); - mPairButton.setVisibility(View.GONE); - setTitle(Html.fromHtml(getString(R.string.chooser_title, - Html.escapeHtml(profileName), - Html.escapeHtml(getCallingAppName())), 0)); - mDeviceListView = findViewById(R.id.device_list); - mDevicesAdapter = new DevicesAdapter(); - mDeviceListView.setAdapter(mDevicesAdapter); - mDeviceListView.setOnItemClickListener((adapterView, view, pos, l) -> { - getService().mSelectedDevice = - (DeviceFilterPair) adapterView.getItemAtPosition(pos); - mDevicesAdapter.notifyDataSetChanged(); - }); - mDevicesAdapter.registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - onSelectionUpdate(); - } - }); - mDeviceListView.addFooterView(mLoadingIndicator = getProgressBar(), null, false); + // mAdapter may be observing - need to remove it. + CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.deleteObservers(); + } + + @Override + protected void onNewIntent(Intent intent) { + // Handle another incoming request (while we are not done with the original - mRequest - + // yet). + final AssociationRequest request = requireNonNull( + intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST)); + if (DEBUG) Log.d(TAG, "onNewIntent(), request=" + request); + + // We can only "process" one request at a time. + final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub + .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); + try { + requireNonNull(appCallback).onFailure("Busy."); + } catch (RemoteException ignore) { } + } + + private void initUI() { + if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest); + + setContentView(R.layout.activity_confirmation); + + mTitle = findViewById(R.id.title); + mSummary = findViewById(R.id.summary); + + mListView = findViewById(R.id.device_list); + mListView.setOnItemClickListener((av, iv, position, id) -> onListItemClick(position)); + + mButtonAllow = findViewById(R.id.button_allow); + mButtonAllow.setOnClickListener(this::onAllowButtonClick); - TextView profileSummary = findViewById(R.id.profile_summary); + findViewById(R.id.button_cancel).setOnClickListener(v -> cancel()); - if (useDeviceProfile) { - profileSummary.setVisibility(View.VISIBLE); - String deviceRef = getRequest().isSingleDevice() - ? getService().mDevicesFound.get(0).getDisplayName() - : profileName; - profileSummary.setText(getString(R.string.profile_summary, - deviceRef, - profilePrivacyDisclaimer)); + final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName()); + if (mRequest.isSelfManaged()) { + initUiForSelfManagedAssociation(appLabel); + } else if (mRequest.isSingleDevice()) { + initUiForSingleDevice(appLabel); } else { - profileSummary.setVisibility(View.GONE); + initUiForMultipleDevices(appLabel); } + } + + private void onAssociationCreated(@NonNull AssociationInfo association) { + if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association); - mCancelButton = findViewById(R.id.button_cancel); - mCancelButton.setOnClickListener(v -> cancel()); + // Don't need to notify the app, CdmService has already done that. Just finish. + setResultAndFinish(association); } - static void notifyDevicesChanged() { - if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) { - sInstance.mDevicesAdapter.notifyDataSetChanged(); + private void cancel() { + if (DEBUG) Log.i(TAG, "cancel()"); + + // Stop discovery service if it was used. + if (!mRequest.isSelfManaged()) { + CompanionDeviceDiscoveryService.stop(this); } - } - private AssociationRequest getRequest() { - return getService().mRequest; + // First send callback to the app directly... + try { + mAppCallback.onFailure("Cancelled."); + } catch (RemoteException ignore) { + } + + // ... then set result and finish ("sending" onActivityResult()). + setResultAndFinish(null); } - private String getDeviceProfileName(@Nullable String deviceProfile) { - if (deviceProfile == null) { - return getString(R.string.profile_name_generic); - } - switch (deviceProfile) { - case AssociationRequest.DEVICE_PROFILE_WATCH: { - return getString(R.string.profile_name_watch); - } - default: { - Log.w(LOG_TAG, - "No localized profile name found for device profile: " + deviceProfile); - return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile) - .toLowerCase() - .replace('_', ' '); + private void setResultAndFinish(@Nullable AssociationInfo association) { + if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association); + + final Intent data = new Intent(); + if (association != null) { + data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association); + if (!association.isSelfManaged()) { + data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, + association.getDeviceMacAddressAsString()); } } - } + setResult(association != null ? RESULT_OK : RESULT_CANCELED, data); - private void cancel() { - Log.i(LOG_TAG, "cancel()"); - getService().onCancel(); - setResult(RESULT_CANCELED); finish(); } - @Override - protected void onStop() { - super.onStop(); - if (!isFinishing() && !isChangingConfigurations()) { - Log.i(LOG_TAG, "onStop() - cancelling"); - cancel(); - } - } + private void initUiForSelfManagedAssociation(CharSequence appLabel) { + if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()"); - @Override - protected void onDestroy() { - super.onDestroy(); - getService().mActivity = null; - if (sInstance == this) { - sInstance = null; + final CharSequence deviceName = mRequest.getDisplayName(); // "<device>"; + final String deviceProfile = mRequest.getDeviceProfile(); // DEVICE_PROFILE_APP_STREAMING; + + final Spanned title; + final Spanned summary; + switch (deviceProfile) { + case DEVICE_PROFILE_APP_STREAMING: + title = getHtmlFromResources(this, R.string.title_app_streaming, appLabel); + summary = getHtmlFromResources( + this, R.string.summary_app_streaming, appLabel, deviceName); + break; + + case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION: + title = getHtmlFromResources(this, R.string.title_automotive_projection, appLabel); + summary = getHtmlFromResources( + this, R.string.summary_automotive_projection, appLabel, deviceName); + break; + + default: + throw new RuntimeException("Unsupported profile " + deviceProfile); } + mTitle.setText(title); + mSummary.setText(summary); + + mListView.setVisibility(View.GONE); } - private CharSequence getCallingAppName() { - try { - final PackageManager packageManager = getPackageManager(); - String callingPackage = Preconditions.checkStringNotEmpty( - getCallingPackage(), - "This activity must be called for result"); - return packageManager.getApplicationLabel( - packageManager.getApplicationInfo(callingPackage, 0)); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); + private void initUiForSingleDevice(CharSequence appLabel) { + if (DEBUG) Log.i(TAG, "initUiFor_SingleDevice()"); + + // TODO: use real name + final String deviceName = "<device>"; + final String deviceProfile = mRequest.getDeviceProfile(); + + final Spanned title = getHtmlFromResources( + this, R.string.confirmation_title, appLabel, deviceName); + final Spanned summary; + if (deviceProfile == null) { + summary = getHtmlFromResources(this, R.string.summary_generic); + } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) { + summary = getHtmlFromResources(this, R.string.summary_watch, appLabel, deviceName); + } else { + throw new RuntimeException("Unsupported profile " + deviceProfile); } - } - @Override - public String getCallingPackage() { - return requireNonNull(getRequest().getCallingPackage()); - } + mTitle.setText(title); + mSummary.setText(summary); - @Override - public void setTitle(CharSequence title) { - final TextView titleView = findViewById(R.id.title); - final int padding = getPadding(getResources()); - titleView.setPadding(padding, padding, padding, padding); - titleView.setText(title); + mListView.setVisibility(View.GONE); } - private ProgressBar getProgressBar() { - final ProgressBar progressBar = new ProgressBar(this); - progressBar.setForegroundGravity(Gravity.CENTER_HORIZONTAL); - final int padding = getPadding(getResources()); - progressBar.setPadding(padding, padding, padding, padding); - return progressBar; - } + private void initUiForMultipleDevices(CharSequence appLabel) { + if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()"); - static int getPadding(Resources r) { - return r.getDimensionPixelSize(R.dimen.padding); - } + final String deviceProfile = mRequest.getDeviceProfile(); - private void onSelectionUpdate() { - DeviceFilterPair selectedDevice = getService().mSelectedDevice; - if (mPairButton.getVisibility() != View.VISIBLE && selectedDevice != null) { - onDeviceConfirmed(selectedDevice); + final String profileName; + final Spanned summary; + if (deviceProfile == null) { + profileName = getString(R.string.profile_name_generic); + summary = getHtmlFromResources(this, R.string.summary_generic); + } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) { + profileName = getString(R.string.profile_name_watch); + summary = getHtmlFromResources(this, R.string.summary_watch, appLabel); } else { - mPairButton.setEnabled(selectedDevice != null); + throw new RuntimeException("Unsupported profile " + deviceProfile); } - } + final Spanned title = getHtmlFromResources( + this, R.string.chooser_title, profileName, appLabel); - private CompanionDeviceDiscoveryService getService() { - return CompanionDeviceDiscoveryService.sInstance; - } + mTitle.setText(title); + mSummary.setText(summary); - protected void onDeviceConfirmed(DeviceFilterPair selectedDevice) { - Log.i(LOG_TAG, "onDeviceConfirmed(selectedDevice = " + selectedDevice + ")"); - getService().onDeviceSelected( - getCallingPackage(), getDeviceMacAddress(selectedDevice.device)); - } + mAdapter = new DeviceListAdapter(this); + CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.addObserver(mAdapter); + // TODO: hide the list and show a spinner until a first device matching device is found. + mListView.setAdapter(mAdapter); - void setResultAndFinish() { - Log.i(LOG_TAG, "setResultAndFinish(selectedDevice = " - + getService().mSelectedDevice.device + ")"); - setResult(RESULT_OK, - new Intent().putExtra( - CompanionDeviceManager.EXTRA_DEVICE, getService().mSelectedDevice.device)); - finish(); + // "Remove" consent button: users would need to click on the list item. + mButtonAllow.setVisibility(View.GONE); } - class DevicesAdapter extends BaseAdapter { - private final Drawable mBluetoothIcon = icon(android.R.drawable.stat_sys_data_bluetooth); - private final Drawable mWifiIcon = icon(com.android.internal.R.drawable.ic_wifi_signal_3); + private void onListItemClick(int position) { + if (DEBUG) Log.d(TAG, "onListItemClick() " + position); - private SparseArray<Integer> mColors = new SparseArray(); + final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position); + final MacAddress macAddress = selectedDevice.getMacAddress(); + onAssociationApproved(macAddress); + } - private Drawable icon(int drawableRes) { - Drawable icon = getResources().getDrawable(drawableRes, null); - icon.setTint(Color.DKGRAY); - return icon; - } + private void onAllowButtonClick(View v) { + if (DEBUG) Log.d(TAG, "onAllowButtonClick()"); - @Override - public View getView( - int position, - @Nullable View convertView, - @NonNull ViewGroup parent) { - TextView view = convertView instanceof TextView - ? (TextView) convertView - : newView(); - bind(view, getItem(position)); - return view; - } + // Disable the button, to prevent more clicks. + v.setEnabled(false); - private void bind(TextView textView, DeviceFilterPair device) { - textView.setText(device.getDisplayName()); - textView.setBackgroundColor( - device.equals(getService().mSelectedDevice) - ? getColor(android.R.attr.colorControlHighlight) - : Color.TRANSPARENT); - textView.setCompoundDrawablesWithIntrinsicBounds( - device.device instanceof android.net.wifi.ScanResult - ? mWifiIcon - : mBluetoothIcon, - null, null, null); - textView.getCompoundDrawables()[0].setTint(getColor(android.R.attr.colorForeground)); + final MacAddress macAddress; + if (mRequest.isSelfManaged()) { + macAddress = null; + } else { + // TODO: implement. + throw new UnsupportedOperationException( + "isSingleDevice() requests are not supported yet."); } + onAssociationApproved(macAddress); + } - private TextView newView() { - final TextView textView = new TextView(CompanionDeviceActivity.this); - textView.setTextColor(getColor(android.R.attr.colorForeground)); - final int padding = CompanionDeviceActivity.getPadding(getResources()); - textView.setPadding(padding, padding, padding, padding); - textView.setCompoundDrawablePadding(padding); - return textView; - } + private void onAssociationApproved(@Nullable MacAddress macAddress) { + if (mAssociationApproved) return; + mAssociationApproved = true; - private int getColor(int colorAttr) { - if (mColors.contains(colorAttr)) { - return mColors.get(colorAttr); - } - TypedValue typedValue = new TypedValue(); - TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { colorAttr }); - int result = a.getColor(0, 0); - a.recycle(); - mColors.put(colorAttr, result); - return result; - } + if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress); - @Override - public int getCount() { - return getService().mDevicesFound.size(); + if (!mRequest.isSelfManaged()) { + requireNonNull(macAddress); + CompanionDeviceDiscoveryService.stop(this); } - @Override - public DeviceFilterPair getItem(int position) { - return getService().mDevicesFound.get(position); + final Bundle data = new Bundle(); + data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest); + data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder()); + if (macAddress != null) { + data.putParcelable(EXTRA_MAC_ADDRESS, macAddress); } - @Override - public long getItemId(int position) { - return position; - } + data.putParcelable(EXTRA_RESULT_RECEIVER, + prepareResultReceiverForIpc(mOnAssociationCreatedReceiver)); + + mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data); } + + private final ResultReceiver mOnAssociationCreatedReceiver = + new ResultReceiver(Handler.getMain()) { + @Override + protected void onReceiveResult(int resultCode, Bundle data) { + if (resultCode != RESULT_CODE_ASSOCIATION_CREATED) { + throw new RuntimeException("Unknown result code: " + resultCode); + } + + final AssociationInfo association = data.getParcelable(EXTRA_ASSOCIATION); + requireNonNull(association); + + onAssociationCreated(association); + } + }; } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index 126b823ab271..a4ff1dc00fda 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -16,18 +16,17 @@ package com.android.companiondevicemanager; -import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; -import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; - +import static com.android.companiondevicemanager.Utils.runOnMainThread; import static com.android.internal.util.ArrayUtils.isEmpty; -import static com.android.internal.util.CollectionUtils.emptyIfNull; -import static com.android.internal.util.CollectionUtils.size; -import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.internal.util.CollectionUtils.filter; +import static com.android.internal.util.CollectionUtils.find; +import static com.android.internal.util.CollectionUtils.map; + +import static java.util.Objects.requireNonNull; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -42,8 +41,6 @@ import android.companion.AssociationRequest; import android.companion.BluetoothDeviceFilter; import android.companion.BluetoothLeDeviceFilter; import android.companion.DeviceFilter; -import android.companion.IAssociationRequestCallback; -import android.companion.ICompanionDeviceDiscoveryService; import android.companion.WifiDeviceFilter; import android.content.BroadcastReceiver; import android.content.Context; @@ -53,417 +50,411 @@ import android.net.wifi.WifiManager; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; -import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; -import com.android.internal.infra.AndroidFuture; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.CollectionUtils; -import com.android.internal.util.Preconditions; - import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Observable; public class CompanionDeviceDiscoveryService extends Service { - private static final boolean DEBUG = false; - private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName(); + private static final String TAG = CompanionDeviceDiscoveryService.class.getSimpleName(); - private static final long SCAN_TIMEOUT = 20000; + private static final String ACTION_START_DISCOVERY = + "com.android.companiondevicemanager.action.START_DISCOVERY"; + private static final String ACTION_STOP_DISCOVERY = + "com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY"; + private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; - static CompanionDeviceDiscoveryService sInstance; + private static final long SCAN_TIMEOUT = 20_000L; // 20 seconds - private BluetoothManager mBluetoothManager; - private BluetoothAdapter mBluetoothAdapter; + // TODO: replace with LiveData-s? + static final Observable TIMEOUT_OBSERVABLE = new MyObservable(); + static final Observable SCAN_RESULTS_OBSERVABLE = new MyObservable(); + + private static CompanionDeviceDiscoveryService sInstance; + + private BluetoothManager mBtManager; + private BluetoothAdapter mBtAdapter; private WifiManager mWifiManager; - @Nullable private BluetoothLeScanner mBLEScanner; - private ScanSettings mDefaultScanSettings = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build(); - - private List<DeviceFilter<?>> mFilters; - private List<BluetoothLeDeviceFilter> mBLEFilters; - private List<BluetoothDeviceFilter> mBluetoothFilters; - private List<WifiDeviceFilter> mWifiFilters; - private List<ScanFilter> mBLEScanFilters; - - AssociationRequest mRequest; - List<DeviceFilterPair> mDevicesFound; - DeviceFilterPair mSelectedDevice; - IAssociationRequestCallback mApplicationCallback; - - AndroidFuture<String> mServiceCallback; - boolean mIsScanning = false; - @Nullable - CompanionDeviceActivity mActivity = null; + private BluetoothLeScanner mBleScanner; - private final ICompanionDeviceDiscoveryService mBinder = - new ICompanionDeviceDiscoveryService.Stub() { - @Override - public void startDiscovery(AssociationRequest request, - String callingPackage, - IAssociationRequestCallback appCallback, - AndroidFuture<String> serviceCallback) { - Log.i(LOG_TAG, - "startDiscovery() called with: filter = [" + request - + "], appCallback = [" + appCallback + "]" - + "], serviceCallback = [" + serviceCallback + "]"); - mApplicationCallback = appCallback; - mServiceCallback = serviceCallback; - Handler.getMain().sendMessage(obtainMessage( - CompanionDeviceDiscoveryService::startDiscovery, - CompanionDeviceDiscoveryService.this, request)); - } + private ScanCallback mBleScanCallback; + private BluetoothBroadcastReceiver mBtReceiver; + private WifiBroadcastReceiver mWifiReceiver; - @Override - public void onAssociationCreated() { - Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated); - } - }; + private boolean mDiscoveryStarted = false; + private boolean mDiscoveryStopped = false; + private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>(); - private ScanCallback mBLEScanCallback; - private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; - private WifiBroadcastReceiver mWifiBroadcastReceiver; + private final Runnable mTimeoutRunnable = this::timeout; - @Override - public IBinder onBind(Intent intent) { - Log.i(LOG_TAG, "onBind(" + intent + ")"); - return mBinder.asBinder(); + static void startForRequest( + @NonNull Context context, @NonNull AssociationRequest associationRequest) { + requireNonNull(associationRequest); + final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class); + intent.setAction(ACTION_START_DISCOVERY); + intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest); + context.startService(intent); + } + + static void stop(@NonNull Context context) { + final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class); + intent.setAction(ACTION_STOP_DISCOVERY); + context.startService(intent); + } + + @MainThread + static @NonNull List<DeviceFilterPair<?>> getScanResults() { + return sInstance != null ? new ArrayList<>(sInstance.mDevicesFound) + : Collections.emptyList(); } @Override public void onCreate() { super.onCreate(); + if (DEBUG) Log.d(TAG, "onCreate()"); - Log.i(LOG_TAG, "onCreate()"); + sInstance = this; - mBluetoothManager = getSystemService(BluetoothManager.class); - mBluetoothAdapter = mBluetoothManager.getAdapter(); - mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); + mBtManager = getSystemService(BluetoothManager.class); + mBtAdapter = mBtManager.getAdapter(); + mBleScanner = mBtAdapter.getBluetoothLeScanner(); mWifiManager = getSystemService(WifiManager.class); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String action = intent.getAction(); + if (DEBUG) Log.d(TAG, "onStartCommand() action=" + action); + + switch (action) { + case ACTION_START_DISCOVERY: + final AssociationRequest request = + intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST); + startDiscovery(request); + break; + + case ACTION_STOP_DISCOVERY: + stopDiscoveryAndFinish(); + break; + } + return START_NOT_STICKY; + } - mDevicesFound = new ArrayList<>(); + @Override + public void onDestroy() { + super.onDestroy(); + if (DEBUG) Log.d(TAG, "onDestroy()"); - sInstance = this; + sInstance = null; } @MainThread - private void startDiscovery(AssociationRequest request) { - if (!request.equals(mRequest)) { - mRequest = request; - - mFilters = request.getDeviceFilters(); - mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); - mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); - mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); - mBLEScanFilters - = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); - - reset(); - } else { - Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); - } + private void startDiscovery(@NonNull AssociationRequest request) { + if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request); + requireNonNull(request); + + if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress."); + mDiscoveryStarted = true; + + final List<DeviceFilter<?>> allFilters = request.getDeviceFilters(); + final List<BluetoothDeviceFilter> btFilters = + filter(allFilters, BluetoothDeviceFilter.class); + final List<BluetoothLeDeviceFilter> bleFilters = + filter(allFilters, BluetoothLeDeviceFilter.class); + final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class); + + checkBoundDevicesIfNeeded(request, btFilters); + + // If no filters are specified: look for everything. + final boolean forceStartScanningAll = isEmpty(allFilters); + // Start BT scanning (if needed) + mBtReceiver = startBtScanningIfNeeded(btFilters, forceStartScanningAll); + // Start Wi-Fi scanning (if needed) + mWifiReceiver = startWifiScanningIfNeeded(wifiFilters, forceStartScanningAll); + // Start BLE scanning (if needed) + mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll); + + // Schedule a time-out. + Handler.getMain().postDelayed(mTimeoutRunnable, SCAN_TIMEOUT); + } - if (!ArrayUtils.isEmpty(mDevicesFound)) { - onReadyToShowUI(); - } + @MainThread + private void stopDiscoveryAndFinish() { + if (DEBUG) Log.i(TAG, "stopDiscovery()"); - // If filtering to get single device by mac address, also search in the set of already - // bonded devices to allow linking those directly - String singleMacAddressFilter = null; - if (mRequest.isSingleDevice()) { - int numFilters = size(mBluetoothFilters); - for (int i = 0; i < numFilters; i++) { - BluetoothDeviceFilter filter = mBluetoothFilters.get(i); - if (!TextUtils.isEmpty(filter.getAddress())) { - singleMacAddressFilter = filter.getAddress(); - break; - } - } - } - if (singleMacAddressFilter != null) { - for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { - onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); - } - for (BluetoothDevice dev : emptyIfNull( - mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) { - onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); - } - for (BluetoothDevice dev : emptyIfNull( - mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) { - onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); - } + if (!mDiscoveryStarted) { + stopSelf(); + return; } - if (shouldScan(mBluetoothFilters)) { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BluetoothDevice.ACTION_FOUND); + if (mDiscoveryStopped) return; + mDiscoveryStopped = true; - Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)"); - mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); - registerReceiver(mBluetoothBroadcastReceiver, intentFilter); - mBluetoothAdapter.startDiscovery(); + // Stop BT discovery. + if (mBtReceiver != null) { + // Cancel discovery. + mBtAdapter.cancelDiscovery(); + // Unregister receiver. + unregisterReceiver(mBtReceiver); + mBtReceiver = null; } - if (shouldScan(mBLEFilters) && mBLEScanner != null) { - Log.i(LOG_TAG, "BLEScanner.startScan"); - mBLEScanCallback = new BLEScanCallback(); - mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); + // Stop Wi-Fi scanning. + if (mWifiReceiver != null) { + // TODO: need to stop scan? + // Unregister receiver. + unregisterReceiver(mWifiReceiver); + mWifiReceiver = null; } - if (shouldScan(mWifiFilters)) { - Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)"); - mWifiBroadcastReceiver = new WifiBroadcastReceiver(); - registerReceiver(mWifiBroadcastReceiver, - new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); - mWifiManager.startScan(); + // Stop BLE scanning. + if (mBleScanCallback != null) { + mBleScanner.stopScan(mBleScanCallback); } - mIsScanning = true; - Handler.getMain().sendMessageDelayed( - obtainMessage(CompanionDeviceDiscoveryService::stopScan, this), - SCAN_TIMEOUT); - } - @MainThread - private void onAssociationCreated() { - mActivity.setResultAndFinish(); - } + Handler.getMain().removeCallbacks(mTimeoutRunnable); - private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) { - return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); + // "Finish". + stopSelf(); } - @MainThread - private void reset() { - Log.i(LOG_TAG, "reset()"); - stopScan(); - mDevicesFound.clear(); - mSelectedDevice = null; - CompanionDeviceActivity.notifyDevicesChanged(); - } + private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request, + @NonNull List<BluetoothDeviceFilter> btFilters) { + // If filtering to get single device by mac address, also search in the set of already + // bonded devices to allow linking those directly + if (btFilters.isEmpty() || !request.isSingleDevice()) return; - @Override - public boolean onUnbind(Intent intent) { - Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")"); - stopScan(); - return super.onUnbind(intent); - } + final BluetoothDeviceFilter singleMacAddressFilter = + find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress())); - private void stopScan() { - Log.i(LOG_TAG, "stopScan()"); + if (singleMacAddressFilter == null) return; - if (!mIsScanning) return; - mIsScanning = false; + findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters); + findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters); + findAndReportMatches( + mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters); + } - if (mActivity != null && mActivity.mDeviceListView != null) { - mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator); - } + private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices, + @NonNull List<BluetoothDeviceFilter> filters) { + if (devices == null) return; - mBluetoothAdapter.cancelDiscovery(); - if (mBluetoothBroadcastReceiver != null) { - unregisterReceiver(mBluetoothBroadcastReceiver); - mBluetoothBroadcastReceiver = null; - } - if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); - if (mWifiBroadcastReceiver != null) { - unregisterReceiver(mWifiBroadcastReceiver); - mWifiBroadcastReceiver = null; + for (BluetoothDevice device : devices) { + final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters); + if (match != null) { + onDeviceFound(match); + } } } - private void onDeviceFound(@Nullable DeviceFilterPair device) { - if (device == null) return; + private BluetoothBroadcastReceiver startBtScanningIfNeeded( + List<BluetoothDeviceFilter> filters, boolean force) { + if (isEmpty(filters) && !force) return null; + if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)"); - Handler.getMain().sendMessage(obtainMessage( - CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device)); - } + final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters); - @MainThread - void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) { - if (mDevicesFound.contains(device)) { - Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices"); - return; - } + final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + registerReceiver(receiver, intentFilter); - Log.i(LOG_TAG, "Found device " + device); + mBtAdapter.startDiscovery(); - if (mDevicesFound.isEmpty()) { - onReadyToShowUI(); - } - mDevicesFound.add(device); - CompanionDeviceActivity.notifyDevicesChanged(); + return receiver; } - //TODO also, on timeout -> call onFailure - private void onReadyToShowUI() { - try { - mApplicationCallback.onAssociationPending(PendingIntent.getActivity( - this, 0, - new Intent(this, CompanionDeviceActivity.class), - PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT - | PendingIntent.FLAG_IMMUTABLE)); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } + private WifiBroadcastReceiver startWifiScanningIfNeeded( + List<WifiDeviceFilter> filters, boolean force) { + if (isEmpty(filters) && !force) return null; + if (DEBUG) Log.d(TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)"); - private void onDeviceLost(@Nullable DeviceFilterPair device) { - Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); - Handler.getMain().sendMessage(obtainMessage( - CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device)); - } + final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters); - @MainThread - void onDeviceLostMainThread(@Nullable DeviceFilterPair device) { - mDevicesFound.remove(device); - CompanionDeviceActivity.notifyDevicesChanged(); - } + final IntentFilter intentFilter = new IntentFilter( + WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + registerReceiver(receiver, intentFilter); - void onDeviceSelected(String callingPackage, String deviceAddress) { - if (callingPackage == null || deviceAddress == null) { - return; - } - mServiceCallback.complete(deviceAddress); - } + mWifiManager.startScan(); - void onCancel() { - if (DEBUG) Log.i(LOG_TAG, "onCancel()"); - mActivity = null; - mServiceCallback.cancel(true); + return receiver; } - /** - * A pair of device and a filter that matched this device if any. - * - * @param <T> device type - */ - static class DeviceFilterPair<T extends Parcelable> { - public final T device; - @Nullable - public final DeviceFilter<T> filter; - - private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { - this.device = device; - this.filter = filter; - } + private ScanCallback startBleScanningIfNeeded( + List<BluetoothLeDeviceFilter> filters, boolean force) { + if (isEmpty(filters) && !force) return null; + if (DEBUG) Log.d(TAG, "BLEScanner.startScan"); - /** - * {@code (device, null)} if the filters list is empty or null - * {@code null} if none of the provided filters match the device - * {@code (device, filter)} where filter is among the list of filters and matches the device - */ - @Nullable - public static <T extends Parcelable> DeviceFilterPair<T> findMatch( - T dev, @Nullable List<? extends DeviceFilter<T>> filters) { - if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); - final DeviceFilter<T> matchingFilter - = CollectionUtils.find(filters, f -> f.matches(dev)); - - DeviceFilterPair<T> result = matchingFilter != null - ? new DeviceFilterPair<>(dev, matchingFilter) - : null; - if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + - ") -> " + result); - return result; + if (mBleScanner == null) { + Log.w(TAG, "BLE Scanner is not available."); + return null; } - public String getDisplayName() { - if (filter == null) { - Preconditions.checkNotNull(device); - if (device instanceof BluetoothDevice) { - return getDeviceDisplayNameInternal((BluetoothDevice) device); - } else if (device instanceof android.net.wifi.ScanResult) { - return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); - } else if (device instanceof ScanResult) { - return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); - } else { - throw new IllegalArgumentException("Unknown device type: " + device.getClass()); + final BLEScanCallback callback = new BLEScanCallback(filters); + + final List<ScanFilter> scanFilters = map( + filters, BluetoothLeDeviceFilter::getScanFilter); + final ScanSettings scanSettings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(); + mBleScanner.startScan(scanFilters, scanSettings, callback); + + return callback; + } + + private void onDeviceFound(@NonNull DeviceFilterPair<?> device) { + runOnMainThread(() -> { + if (DEBUG) Log.v(TAG, "onDeviceFound() " + device); + if (mDevicesFound.contains(device)) { + // TODO: update the device instead of ignoring (new found device may contain + // additional/updated info, eg. name of the device). + if (DEBUG) { + Log.d(TAG, "onDeviceFound() " + device.toShortString() + + " - Already seen: ignore."); } + return; } - return filter.getDeviceDisplayName(device); - } + if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device."); - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; - return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); - } + // First: make change. + mDevicesFound.add(device); + // Then: notify observers. + SCAN_RESULTS_OBSERVABLE.notifyObservers(); + }); + } - @Override - public int hashCode() { - return Objects.hash(getDeviceMacAddress(device)); - } + private void onDeviceLost(@Nullable DeviceFilterPair<?> device) { + runOnMainThread(() -> { + if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString()); - @Override - public String toString() { - return "DeviceFilterPair{" - + "device=" + device + " " + getDisplayName() - + ", filter=" + filter - + '}'; - } + // First: make change. + mDevicesFound.remove(device); + // Then: notify observers. + SCAN_RESULTS_OBSERVABLE.notifyObservers(); + }); + } + + private void timeout() { + if (DEBUG) Log.i(TAG, "timeout()"); + stopDiscoveryAndFinish(); + TIMEOUT_OBSERVABLE.notifyObservers(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; } private class BLEScanCallback extends ScanCallback { + final List<BluetoothLeDeviceFilter> mFilters; - public BLEScanCallback() { - if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); + BLEScanCallback(List<BluetoothLeDeviceFilter> filters) { + mFilters = filters; } @Override public void onScanResult(int callbackType, ScanResult result) { if (DEBUG) { - Log.i(LOG_TAG, - "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result - + ")"); + Log.v(TAG, "BLE.onScanResult() callback=" + callbackType + ", result=" + result); } - final DeviceFilterPair<ScanResult> deviceFilterPair - = DeviceFilterPair.findMatch(result, mBLEFilters); - if (deviceFilterPair == null) return; + + final DeviceFilterPair<ScanResult> match = findMatch(result, mFilters); + if (match == null) return; + if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { - onDeviceLost(deviceFilterPair); + onDeviceLost(match); } else { - onDeviceFound(deviceFilterPair); + // TODO: check this logic. + onDeviceFound(match); } } } private class BluetoothBroadcastReceiver extends BroadcastReceiver { + final List<BluetoothDeviceFilter> mFilters; + + BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters) { + this.mFilters = filters; + } + @Override public void onReceive(Context context, Intent intent) { - if (DEBUG) { - Log.i(LOG_TAG, - "BL.onReceive(context = " + context + ", intent = " + intent + ")"); - } + final String action = intent.getAction(); final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - final DeviceFilterPair<BluetoothDevice> deviceFilterPair - = DeviceFilterPair.findMatch(device, mBluetoothFilters); - if (deviceFilterPair == null) return; - if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { - onDeviceFound(deviceFilterPair); + + if (DEBUG) Log.v(TAG, action + ", device=" + device); + + if (action == null) return; + + final DeviceFilterPair<BluetoothDevice> match = findMatch(device, mFilters); + if (match == null) return; + + if (action.equals(BluetoothDevice.ACTION_FOUND)) { + onDeviceFound(match); } else { - onDeviceLost(deviceFilterPair); + // TODO: check this logic. + onDeviceLost(match); } } } private class WifiBroadcastReceiver extends BroadcastReceiver { + final List<WifiDeviceFilter> mFilters; + + private WifiBroadcastReceiver(List<WifiDeviceFilter> filters) { + this.mFilters = filters; + } + @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { - List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); + if (!Objects.equals(intent.getAction(), WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + return; + } - if (DEBUG) { - Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); - } + final List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); + if (DEBUG) { + Log.v(TAG, "WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, results:\n " + + TextUtils.join("\n ", scanResults)); + } - for (int i = 0; i < scanResults.size(); i++) { - onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); + for (int i = 0; i < scanResults.size(); i++) { + final android.net.wifi.ScanResult scanResult = scanResults.get(i); + final DeviceFilterPair<?> match = findMatch(scanResult, mFilters); + if (match != null) { + onDeviceFound(match); } } } } + + /** + * {@code (device, null)} if the filters list is empty or null + * {@code null} if none of the provided filters match the device + * {@code (device, filter)} where filter is among the list of filters and matches the device + */ + @Nullable + public static <T extends Parcelable> DeviceFilterPair<T> findMatch( + T dev, @Nullable List<? extends DeviceFilter<T>> filters) { + if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); + final DeviceFilter<T> matchingFilter = find(filters, f -> f.matches(dev)); + + DeviceFilterPair<T> result = matchingFilter != null + ? new DeviceFilterPair<>(dev, matchingFilter) : null; + if (DEBUG) { + Log.v(TAG, "findMatch(dev=" + dev + ", filters=" + filters + ") -> " + result); + } + return result; + } + + private static class MyObservable extends Observable { + @Override + public void notifyObservers() { + setChanged(); + super.notifyObservers(); + } + } } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java new file mode 100644 index 000000000000..faca1ae3f058 --- /dev/null +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2021 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.companiondevicemanager; + +import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; +import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothDevice; +import android.companion.DeviceFilter; +import android.net.MacAddress; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * A pair of device and a filter that matched this device if any. + * + * @param <T> device type. + */ +class DeviceFilterPair<T extends Parcelable> { + private final T mDevice; + private final @Nullable DeviceFilter<T> mFilter; + + DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { + this.mDevice = device; + this.mFilter = filter; + } + + T getDevice() { + return mDevice; + } + + String getDisplayName() { + if (mFilter != null) mFilter.getDeviceDisplayName(mDevice); + + if (mDevice instanceof BluetoothDevice) { + return getDeviceDisplayNameInternal((BluetoothDevice) mDevice); + } else if (mDevice instanceof android.bluetooth.le.ScanResult) { + final android.bluetooth.le.ScanResult bleScanResult = + (android.bluetooth.le.ScanResult) mDevice; + return getDeviceDisplayNameInternal(bleScanResult.getDevice()); + } else if (mDevice instanceof android.net.wifi.ScanResult) { + final android.net.wifi.ScanResult wifiScanResult = + (android.net.wifi.ScanResult) mDevice; + return getDeviceDisplayNameInternal(wifiScanResult); + } else { + throw new IllegalArgumentException("Unknown device type: " + mDevice.getClass()); + } + } + + @NonNull MacAddress getMacAddress() { + return MacAddress.fromString(getDeviceMacAddress(getDevice())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; + return Objects.equals(getDeviceMacAddress(mDevice), getDeviceMacAddress(that.mDevice)); + } + + @Override + public int hashCode() { + return Objects.hash(getDeviceMacAddress(mDevice)); + } + + @Override + public String toString() { + return "DeviceFilterPair{" + + "device=" + mDevice + " " + getDisplayName() + + ", filter=" + mFilter + + '}'; + } + + @NonNull String toShortString() { + return '(' + getDeviceTypeAsString() + ") " + getMacAddress() + " '" + getDisplayName() + + '\''; + } + + private @NonNull String getDeviceTypeAsString() { + if (mDevice instanceof BluetoothDevice) { + return "BT"; + } else if (mDevice instanceof android.bluetooth.le.ScanResult) { + return "BLE"; + } else if (mDevice instanceof android.net.wifi.ScanResult) { + return "Wi-Fi"; + } else { + return "Unknown"; + } + } +} diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java new file mode 100644 index 000000000000..cf2a2bfa468b --- /dev/null +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 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.companiondevicemanager; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.List; +import java.util.Observable; +import java.util.Observer; + +/** + * Adapter for the list of "found" devices. + */ +class DeviceListAdapter extends BaseAdapter implements Observer { + private final Context mContext; + private final Resources mResources; + + private final Drawable mBluetoothIcon; + private final Drawable mWifiIcon; + + private final @ColorInt int mTextColor; + + // List if pairs (display name, address) + private List<DeviceFilterPair<?>> mDevices; + + DeviceListAdapter(Context context) { + mContext = context; + mResources = context.getResources(); + mBluetoothIcon = getTintedIcon(mResources, android.R.drawable.stat_sys_data_bluetooth); + mWifiIcon = getTintedIcon(mResources, com.android.internal.R.drawable.ic_wifi_signal_3); + mTextColor = getColor(context, android.R.attr.colorForeground); + } + + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + final TextView view = convertView != null ? (TextView) convertView : newView(); + bind(view, getItem(position)); + return view; + } + + private void bind(TextView textView, DeviceFilterPair<?> item) { + textView.setText(item.getDisplayName()); + textView.setBackgroundColor(Color.TRANSPARENT); + /* + textView.setCompoundDrawablesWithIntrinsicBounds( + item.getDevice() instanceof android.net.wifi.ScanResult + ? mWifiIcon + : mBluetoothIcon, + null, null, null); + textView.getCompoundDrawables()[0].setTint(mTextColor); + */ + } + + private TextView newView() { + final TextView textView = new TextView(mContext); + textView.setTextColor(mTextColor); + final int padding = 24; + textView.setPadding(padding, padding, padding, padding); + //textView.setCompoundDrawablePadding(padding); + return textView; + } + + @Override + public int getCount() { + return mDevices != null ? mDevices.size() : 0; + } + + @Override + public DeviceFilterPair<?> getItem(int position) { + return mDevices.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public void update(Observable o, Object arg) { + mDevices = CompanionDeviceDiscoveryService.getScanResults(); + notifyDataSetChanged(); + } + + private @ColorInt int getColor(Context context, int attr) { + final TypedArray a = context.obtainStyledAttributes(new TypedValue().data, + new int[] { attr }); + final int color = a.getColor(0, 0); + a.recycle(); + return color; + } + + private static Drawable getTintedIcon(Resources resources, int drawableRes) { + Drawable icon = resources.getDrawable(drawableRes, null); + icon.setTint(Color.DKGRAY); + return icon; + } +} diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java new file mode 100644 index 000000000000..eab421e48446 --- /dev/null +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 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.companiondevicemanager; + +import android.annotation.NonNull; +import android.annotation.StringRes; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.ResultReceiver; +import android.text.Html; +import android.text.Spanned; + +/** + * Utilities. + */ +class Utils { + + /** + * Convert an instance of a "locally-defined" ResultReceiver to an instance of + * {@link android.os.ResultReceiver} itself, which the receiving process will be able to + * unmarshall. + */ + static <T extends ResultReceiver> ResultReceiver prepareResultReceiverForIpc(T resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return ipcFriendly; + } + + static @NonNull CharSequence getApplicationLabel( + @NonNull Context context, @NonNull String packageName) { + final PackageManager packageManager = context.getPackageManager(); + final ApplicationInfo appInfo; + try { + appInfo = packageManager.getApplicationInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + return packageManager.getApplicationLabel(appInfo); + } + + static Spanned getHtmlFromResources( + @NonNull Context context, @StringRes int resId, CharSequence... formatArgs) { + final String[] escapedArgs = new String[formatArgs.length]; + for (int i = 0; i < escapedArgs.length; i++) { + escapedArgs[i] = Html.escapeHtml(formatArgs[i]); + } + final String plain = context.getString(resId, (Object[]) escapedArgs); + return Html.fromHtml(plain, 0); + } + + static void runOnMainThread(Runnable runnable) { + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { + runnable.run(); + } else { + Handler.getMain().post(runnable); + } + } + + private Utils() { + } +} diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index bcc345ff98e4..637994f6d73d 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -16,8 +16,13 @@ package com.android.server.companion; +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_ONE_SHOT; +import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME; +import static android.content.ComponentName.createRelative; + import static com.android.internal.util.CollectionUtils.filter; -import static com.android.internal.util.FunctionalUtils.uncheckExceptions; import static com.android.server.companion.CompanionDeviceManagerService.DEBUG; import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG; import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation; @@ -28,73 +33,114 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.AssociationRequest; -import android.companion.CompanionDeviceManager; import android.companion.IAssociationRequestCallback; -import android.companion.ICompanionDeviceDiscoveryService; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageManagerInternal; import android.content.pm.Signature; +import android.net.MacAddress; import android.os.Binder; -import android.os.IBinder; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; import android.os.RemoteException; +import android.os.ResultReceiver; import android.util.PackageUtils; import android.util.Slog; -import com.android.internal.infra.AndroidFuture; -import com.android.internal.infra.PerUser; -import com.android.internal.infra.ServiceConnector; import com.android.internal.util.ArrayUtils; -import com.android.server.FgThread; -import java.io.PrintWriter; import java.util.Arrays; import java.util.HashSet; -import java.util.Objects; import java.util.Set; +/** + * Class responsible for handling incoming {@link AssociationRequest}s. + * The main responsibilities of an {@link AssociationRequestsProcessor} are: + * <ul> + * <li> Requests validation and checking if the package that would own the association holds all + * necessary permissions. + * <li> Communication with the requester via a provided + * {@link android.companion.CompanionDeviceManager.Callback}. + * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the + * approval. + * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the + * request was found valid and was approved by user. + * </ul> + * + * The class supports two variants of the "Association Flow": the full variant, and the shortened + * (a.k.a. No-UI) variant. + * Both flows start similarly: in + * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)} + * invoked from + * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)} + * method call. + * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is + * required. + * + * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes + * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)} + * which after calling to {@link CompanionDeviceManagerService} to create an association, notifies + * the requester via + * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}. + * + * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a + * {@link PendingIntent} for the approval UI and sends it back to the requester via + * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}. + * When/if user approves the request, {@link AssociationRequestsProcessor} receives a "callback" + * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes + * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)} + * which one more time checks that the packages holds all necessary permissions before proceeding to + * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}. + * + * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback) + * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, + * ResultReceiver, MacAddress) + */ class AssociationRequestsProcessor { private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor"; - private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative( - CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, - ".CompanionDeviceDiscoveryService"); + private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY = + createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity"); + + // AssociationRequestsProcessor <-> UI + private static final String EXTRA_APPLICATION_CALLBACK = "application_callback"; + private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; + private static final String EXTRA_RESULT_RECEIVER = "result_receiver"; + + // AssociationRequestsProcessor -> UI + private static final int RESULT_CODE_ASSOCIATION_CREATED = 0; + private static final String EXTRA_ASSOCIATION = "association"; + + // UI -> AssociationRequestsProcessor + private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0; + private static final String EXTRA_MAC_ADDRESS = "mac_address"; private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5; private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min; private final Context mContext; private final CompanionDeviceManagerService mService; - private final PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors; - - private AssociationRequest mRequest; - private IAssociationRequestCallback mAppCallback; - private AndroidFuture<?> mOngoingDeviceDiscovery; + private final PackageManagerInternal mPackageManager; AssociationRequestsProcessor(CompanionDeviceManagerService service) { mContext = service.getContext(); mService = service; - - final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO); - mServiceConnectors = new PerUser<>() { - @Override - protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) { - return new ServiceConnector.Impl<>( - mContext, - serviceIntent, 0/* bindingFlags */, userId, - ICompanionDeviceDiscoveryService.Stub::asInterface); - } - }; + mPackageManager = service.mPackageManagerInternal; } /** * Handle incoming {@link AssociationRequest}s, sent via * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)} */ - void process(@NonNull AssociationRequest request, @NonNull String packageName, - @UserIdInt int userId, @NonNull IAssociationRequestCallback callback) { + void processNewAssociationRequest(@NonNull AssociationRequest request, + @NonNull String packageName, @UserIdInt int userId, + @NonNull IAssociationRequestCallback callback) { requireNonNull(request, "Request MUST NOT be null"); if (request.isSelfManaged()) { requireNonNull(request.getDisplayName(), "AssociationRequest.displayName " @@ -103,14 +149,15 @@ class AssociationRequestsProcessor { requireNonNull(packageName, "Package name MUST NOT be null"); requireNonNull(callback, "Callback MUST NOT be null"); + final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); if (DEBUG) { - Slog.d(TAG, "process() " + Slog.d(TAG, "processNewAssociationRequest() " + "request=" + request + ", " - + "package=u" + userId + "/" + packageName); + + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")"); } // 1. Enforce permissions and other requirements. - enforcePermissionsForAssociation(mContext, request, packageName, userId); + enforcePermissionsForAssociation(mContext, request, packageUid); mService.checkUsesFeature(packageName, userId); // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER @@ -118,71 +165,99 @@ class AssociationRequestsProcessor { if (request.isSelfManaged() && !request.isForceConfirmation() && !willAddRoleHolder(request, packageName, userId)) { // 2a. Create association right away. - final AssociationInfo association = mService.createAssociation(userId, packageName, - /* macAddress */ null, request.getDisplayName(), request.getDeviceProfile(), - /* selfManaged */true); - withCatchingRemoteException(() -> callback.onAssociationCreated(association)); + createAssociationAndNotifyApplication(request, packageName, userId, + /*macAddress*/ null, callback); return; } - // 2b. Launch the UI. - synchronized (mService.mLock) { - if (mRequest != null) { - Slog.w(TAG, "CDM is already processing another AssociationRequest."); + // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app: - withCatchingRemoteException(() -> callback.onFailure("Busy.")); - } + // 2b.1. Populate the request with required info. + request.setPackageName(packageName); + request.setUserId(userId); + request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId)); - final boolean linked = withCatchingRemoteException( - () -> callback.asBinder().linkToDeath(mBinderDeathRecipient, 0)); - if (!linked) { - // The process has died by now: do not proceed. - return; - } + // 2b.2. Prepare extras and create an Intent. + final Bundle extras = new Bundle(); + extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request); + extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder()); + extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver)); - mRequest = request; + final Intent intent = new Intent(); + intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY); + intent.putExtras(extras); + + // 2b.3. Create a PendingIntent. + final PendingIntent pendingIntent; + final long token = Binder.clearCallingIdentity(); + try { + // Using uid of the application that will own the association (usually the same + // application that sent the request) allows us to have multiple "pending" association + // requests at the same time. + // If the application already has a pending association request, that PendingIntent + // will be cancelled. + pendingIntent = PendingIntent.getActivity(mContext, /*requestCode */ packageUid, intent, + FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); + } finally { + Binder.restoreCallingIdentity(token); } - mAppCallback = callback; - request.setCallingPackage(packageName); + // 2b.4. Send the PendingIntent back to the app. + try { + callback.onAssociationPending(pendingIntent); + } catch (RemoteException ignore) { } + } - if (mayAssociateWithoutPrompt(packageName, userId)) { - Slog.i(TAG, "setSkipPrompt(true)"); - request.setSkipPrompt(true); + private void processAssociationRequestApproval(@NonNull AssociationRequest request, + @NonNull IAssociationRequestCallback callback, + @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) { + final String packageName = request.getPackageName(); + final int userId = request.getUserId(); + final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); + + if (DEBUG) { + Slog.d(TAG, "processAssociationRequestApproval()\n" + + " package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n" + + " request=" + request + "\n" + + " macAddress=" + macAddress + "\n"); } - final String deviceProfile = request.getDeviceProfile(); - mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile) - .thenComposeAsync(description -> { - if (DEBUG) { - Slog.d(TAG, "fetchProfileDescription done: " + description); - } - - request.setDeviceProfilePrivilegesDescription(description); - - return mServiceConnectors.forUser(userId).postAsync(service -> { - if (DEBUG) { - Slog.d(TAG, "Connected to CDM service -> " - + "Starting discovery for " + request); - } - - AndroidFuture<String> future = new AndroidFuture<>(); - service.startDiscovery(request, packageName, callback, future); - return future; - }).cancelTimeout(); - - }, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> { - if (err == null) { - mService.legacyCreateAssociation( - userId, deviceAddress, packageName, deviceProfile); - mServiceConnectors.forUser(userId).post( - ICompanionDeviceDiscoveryService::onAssociationCreated); - } else { - Slog.e(TAG, "Failed to discover device(s)", err); - callback.onFailure("No devices found: " + err.getMessage()); - } - cleanup(); - })); + // 1. Need to check permissions again in case something changed, since we first received + // this request. + try { + enforcePermissionsForAssociation(mContext, request, packageUid); + } catch (SecurityException e) { + // Since, at this point the caller is our own UI, we need to catch the exception on + // forward it back to the application via the callback. + try { + callback.onFailure(e.getMessage()); + } catch (RemoteException ignore) { } + return; + } + + // 2. Create association and notify the application. + final AssociationInfo association = createAssociationAndNotifyApplication( + request, packageName, userId, macAddress, callback); + + // 3. Send the association back the Approval Activity, so that it can report back to the app + // via Activity.setResult(). + final Bundle data = new Bundle(); + data.putParcelable(EXTRA_ASSOCIATION, association); + resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data); + } + + private AssociationInfo createAssociationAndNotifyApplication( + @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, + @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) { + final AssociationInfo association = mService.createAssociation(userId, packageName, + macAddress, request.getDisplayName(), request.getDeviceProfile(), + request.isSelfManaged()); + + try { + callback.onAssociationCreated(association); + } catch (RemoteException ignore) { } + + return association; } private boolean willAddRoleHolder(@NonNull AssociationRequest request, @@ -197,26 +272,44 @@ class AssociationRequestsProcessor { return !isRoleHolder; } - private void cleanup() { - if (DEBUG) { - Slog.d(TAG, "cleanup(); discovery = " - + mOngoingDeviceDiscovery + ", request = " + mRequest); - } - synchronized (mService.mLock) { - AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery; - if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) { - ongoingDeviceDiscovery.cancel(true); + private final ResultReceiver mOnRequestConfirmationReceiver = + new ResultReceiver(Handler.getMain()) { + @Override + protected void onReceiveResult(int resultCode, Bundle data) { + if (DEBUG) { + Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() " + + "code=" + resultCode + ", " + "data=" + data); } - if (mAppCallback != null) { - mAppCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0); - mAppCallback = null; + + if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) { + Slog.w(TAG, "Unknown result code:" + resultCode); + return; } - mRequest = null; + + final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST); + final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub + .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK)); + final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER); + + requireNonNull(request); + requireNonNull(callback); + requireNonNull(resultReceiver); + + final MacAddress macAddress; + if (request.isSelfManaged()) { + macAddress = null; + } else { + macAddress = data.getParcelable(EXTRA_MAC_ADDRESS); + requireNonNull(macAddress); + } + + processAssociationRequestApproval(request, callback, resultReceiver, macAddress); } - } + }; - private boolean mayAssociateWithoutPrompt(String packageName, int userId) { - final String deviceProfile = mRequest.getDeviceProfile(); + private boolean mayAssociateWithoutPrompt(@NonNull AssociationRequest request, + @NonNull String packageName, @UserIdInt int userId) { + final String deviceProfile = request.getDeviceProfile(); if (deviceProfile != null) { final boolean isRoleHolder = Binder.withCleanCallingIdentity( () -> isRoleHolder(mContext, userId, packageName, deviceProfile)); @@ -252,8 +345,8 @@ class AssociationRequestsProcessor { String[] sameOemCerts = mContext.getResources() .getStringArray(com.android.internal.R.array.config_companionDeviceCerts); - Signature[] signatures = mService.mPackageManagerInternal - .getPackage(packageName).getSigningDetails().getSignatures(); + Signature[] signatures = mPackageManager.getPackage(packageName).getSigningDetails() + .getSignatures(); String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures); Set<String> sameOemPackageCerts = @@ -274,47 +367,6 @@ class AssociationRequestsProcessor { return false; } - @NonNull - private AndroidFuture<String> getDeviceProfilePermissionDescription( - @Nullable String deviceProfile) { - if (deviceProfile == null) { - return AndroidFuture.completedFuture(null); - } - - final AndroidFuture<String> result = new AndroidFuture<>(); - mService.mPermissionControllerManager.getPrivilegesDescriptionStringForProfile( - deviceProfile, FgThread.getExecutor(), desc -> { - try { - result.complete(String.valueOf(desc)); - } catch (Exception e) { - result.completeExceptionally(e); - } - }); - return result; - } - - - void dump(@NonNull PrintWriter pw) { - pw.append("Discovery Service State:").append('\n'); - for (int i = 0, size = mServiceConnectors.size(); i < size; i++) { - int userId = mServiceConnectors.keyAt(i); - pw.append(" ") - .append("u").append(Integer.toString(userId)).append(": ") - .append(Objects.toString(mServiceConnectors.valueAt(i))) - .append('\n'); - } - } - - private final IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - if (DEBUG) { - Slog.d(TAG, "binderDied()"); - } - mService.mMainHandler.post(AssociationRequestsProcessor.this::cleanup); - } - }; - private static Set<String> getSameOemPackageCerts( String packageName, String[] oemPackages, String[] sameOemCerts) { Set<String> sameOemPackageCerts = new HashSet<>(); @@ -330,16 +382,19 @@ class AssociationRequestsProcessor { return sameOemPackageCerts; } - private static boolean withCatchingRemoteException(ThrowingRunnable runnable) { - try { - runnable.run(); - } catch (RemoteException e) { - return false; - } - return true; - } + /** + * Convert an instance of a "locally-defined" ResultReceiver to an instance of + * {@link android.os.ResultReceiver} itself, which the receiving process will be able to + * unmarshall. + */ + private static <T extends ResultReceiver> ResultReceiver prepareForIpc(T resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); - private interface ThrowingRunnable { - void run() throws RemoteException; + return ipcFriendly; } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 9c996f452cf6..626128aeba2a 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -406,7 +406,8 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName, "create associations"); - mAssociationRequestsProcessor.process(request, packageName, userId, callback); + mAssociationRequestsProcessor.processNewAssociationRequest( + request, packageName, userId, callback); } @Override @@ -717,8 +718,6 @@ public class CompanionDeviceManagerService extends SystemService { .append(sDateFormat.format(time)).append('\n'); } - mAssociationRequestsProcessor.dump(fout); - fout.append("Device Listener Services State:").append('\n'); for (int i = 0, size = mCompanionDevicePresenceController.mBoundServices.size(); i < size; i++) { diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java index ea570895c18f..3a8ee7398329 100644 --- a/services/companion/java/com/android/server/companion/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java @@ -38,13 +38,11 @@ import android.annotation.UserIdInt; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.content.Context; -import android.content.pm.PackageManagerInternal; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import com.android.internal.app.IAppOpsService; -import com.android.server.LocalServices; import java.util.Map; @@ -69,9 +67,7 @@ final class PermissionsUtils { } static void enforcePermissionsForAssociation(@NonNull Context context, - @NonNull AssociationRequest request, @NonNull String packageName, - @UserIdInt int userId) { - final int packageUid = getPackageUid(userId, packageName); + @NonNull AssociationRequest request, int packageUid) { enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile(), packageUid); if (request.isSelfManaged()) { @@ -207,11 +203,6 @@ final class PermissionsUtils { } } - private static int getPackageUid(@UserIdInt int userId, @NonNull String packageName) { - return LocalServices.getService(PackageManagerInternal.class) - .getPackageUid(packageName, 0, userId); - } - private static IAppOpsService getAppOpsService() { if (sAppOpsService == null) { synchronized (PermissionsUtils.class) { |