Merge "Update CMD association flow."
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
index 6e1f8b5..18a59d8 100644
--- a/core/java/android/companion/AssociationRequest.java
+++ b/core/java/android/companion/AssociationRequest.java
@@ -20,12 +20,15 @@
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 @@
* 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 @@
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 mCallingPackage;
+ private @Nullable String mPackageName;
+
+ /**
+ * The UserId of the user the association will belong to.
+ * Populated by the system.
+ * @hide
+ */
+ 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 @@
}
/** @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 @@
/** @hide */
public void setSkipPrompt(boolean value) {
- mSkipPrompt = true;
+ mSkipPrompt = value;
}
/** @hide */
@@ -258,10 +301,6 @@
return mDeviceFilters;
}
- private void onConstructed() {
- mCreationTime = System.currentTimeMillis();
- }
-
/**
* A builder for {@link AssociationRequest}
*/
@@ -325,7 +364,7 @@
@NonNull
public Builder setDisplayName(@NonNull CharSequence displayName) {
checkNotUsed();
- mDisplayName = Objects.requireNonNull(displayName);
+ mDisplayName = requireNonNull(displayName);
return this;
}
@@ -372,15 +411,13 @@
+ "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 @@
/**
- * 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.
- *
- * 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();
- }
-
- /**
- * The app package making the request.
- *
+ * The app package name of the application the association will belong to.
* Populated by the system.
*
* @hide
*/
@DataClass.Generated.Member
- public @Nullable String getCallingPackage() {
- return mCallingPackage;
+ public @Nullable String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * The UserId of the user the association will belong to.
+ * Populated by the system.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ 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 @@
/**
* Whether the user-prompt may be skipped once the device is found.
- *
* Populated by the system.
*
* @hide
@@ -521,7 +498,8 @@
"displayName = " + mDisplayName + ", " +
"selfManaged = " + mSelfManaged + ", " +
"forceConfirmation = " + mForceConfirmation + ", " +
- "callingPackage = " + mCallingPackage + ", " +
+ "packageName = " + mPackageName + ", " +
+ "userId = " + mUserId + ", " +
"deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " +
"creationTime = " + mCreationTime + ", " +
"skipPrompt = " + mSkipPrompt +
@@ -547,7 +525,8 @@
&& 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 @@
_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 @@
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 @@
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 @@
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 @@
};
@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 c5926a5..06f2d9d 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 0000000..c87bac6
--- /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 a80720c..0000000
--- 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 273347a..0000000
--- 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 1336e79..0000000
--- 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 80fec59..0000000
--- 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 9a50366..0000000
--- 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 da7b0d1..0000000
--- 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 44748e9..cb8b616 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>
- <!-- 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>
+ <!-- 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 ================= -->
+
+ <!-- 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 9dced47b..0000000
--- 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 a5168cc..cc887c3 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();
- static CompanionDeviceActivity sInstance;
+ // Keep the following constants in sync with
+ // frameworks/base/services/companion/java/
+ // com/android/server/companion/AssociationRequestsProcessor.java
- View mLoadingIndicator = null;
- ListView mDeviceListView;
- private View mPairButton;
- private View mCancelButton;
+ // 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";
- DevicesAdapter mDevicesAdapter;
+ // 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 AssociationRequest mRequest;
+ private IAssociationRequestCallback mAppCallback;
+ private ResultReceiver mCdmServiceReceiver;
+
+ // 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);
-
- Log.i(LOG_TAG, "Starting UI for " + getService().mRequest);
-
- if (getService().mDevicesFound.isEmpty()) {
- Log.e(LOG_TAG, "About to show UI, but no devices to show");
- }
-
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);
- }
-
- TextView profileSummary = findViewById(R.id.profile_summary);
-
- 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));
- } else {
- profileSummary.setVisibility(View.GONE);
- }
-
- mCancelButton = findViewById(R.id.button_cancel);
- mCancelButton.setOnClickListener(v -> cancel());
}
- static void notifyDevicesChanged() {
- if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) {
- sInstance.mDevicesAdapter.notifyDataSetChanged();
- }
- }
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (DEBUG) Log.d(TAG, "onStart()");
- private AssociationRequest getRequest() {
- return getService().mRequest;
- }
+ 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);
- 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('_', ' ');
- }
- }
- }
+ requireNonNull(mRequest);
+ requireNonNull(mAppCallback);
+ requireNonNull(mCdmServiceReceiver);
- private void cancel() {
- Log.i(LOG_TAG, "cancel()");
- getService().onCancel();
- setResult(RESULT_CANCELED);
- finish();
+ // Start discovery services if needed.
+ if (!mRequest.isSelfManaged()) {
+ CompanionDeviceDiscoveryService.startForRequest(this, mRequest);
+ }
+ // Init UI.
+ initUI();
}
@Override
protected void onStop() {
super.onStop();
- if (!isFinishing() && !isChangingConfigurations()) {
- Log.i(LOG_TAG, "onStop() - cancelling");
- cancel();
+ if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing());
+
+ // TODO: handle config changes without cancelling.
+ if (!isFinishing()) {
+ cancel(); // will finish()
}
+
+ // mAdapter may be observing - need to remove it.
+ CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.deleteObservers();
}
@Override
- protected void onDestroy() {
- super.onDestroy();
- getService().mActivity = null;
- if (sInstance == this) {
- sInstance = null;
- }
- }
+ 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);
- private CharSequence getCallingAppName() {
+ // We can only "process" one request at a time.
+ final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub
+ .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK));
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);
+ requireNonNull(appCallback).onFailure("Busy.");
+ } catch (RemoteException ignore) {
}
}
- @Override
- public String getCallingPackage() {
- return requireNonNull(getRequest().getCallingPackage());
- }
+ private void initUI() {
+ if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest);
- @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);
- }
+ setContentView(R.layout.activity_confirmation);
- 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;
- }
+ mTitle = findViewById(R.id.title);
+ mSummary = findViewById(R.id.summary);
- static int getPadding(Resources r) {
- return r.getDimensionPixelSize(R.dimen.padding);
- }
+ mListView = findViewById(R.id.device_list);
+ mListView.setOnItemClickListener((av, iv, position, id) -> onListItemClick(position));
- private void onSelectionUpdate() {
- DeviceFilterPair selectedDevice = getService().mSelectedDevice;
- if (mPairButton.getVisibility() != View.VISIBLE && selectedDevice != null) {
- onDeviceConfirmed(selectedDevice);
+ mButtonAllow = findViewById(R.id.button_allow);
+ mButtonAllow.setOnClickListener(this::onAllowButtonClick);
+
+ findViewById(R.id.button_cancel).setOnClickListener(v -> cancel());
+
+ final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName());
+ if (mRequest.isSelfManaged()) {
+ initUiForSelfManagedAssociation(appLabel);
+ } else if (mRequest.isSingleDevice()) {
+ initUiForSingleDevice(appLabel);
} else {
- mPairButton.setEnabled(selectedDevice != null);
+ initUiForMultipleDevices(appLabel);
}
}
- private CompanionDeviceDiscoveryService getService() {
- return CompanionDeviceDiscoveryService.sInstance;
+ private void onAssociationCreated(@NonNull AssociationInfo association) {
+ if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
+
+ // Don't need to notify the app, CdmService has already done that. Just finish.
+ setResultAndFinish(association);
}
- protected void onDeviceConfirmed(DeviceFilterPair selectedDevice) {
- Log.i(LOG_TAG, "onDeviceConfirmed(selectedDevice = " + selectedDevice + ")");
- getService().onDeviceSelected(
- getCallingPackage(), getDeviceMacAddress(selectedDevice.device));
+ private void cancel() {
+ if (DEBUG) Log.i(TAG, "cancel()");
+
+ // Stop discovery service if it was used.
+ if (!mRequest.isSelfManaged()) {
+ CompanionDeviceDiscoveryService.stop(this);
+ }
+
+ // First send callback to the app directly...
+ try {
+ mAppCallback.onFailure("Cancelled.");
+ } catch (RemoteException ignore) {
+ }
+
+ // ... then set result and finish ("sending" onActivityResult()).
+ setResultAndFinish(null);
}
- void setResultAndFinish() {
- Log.i(LOG_TAG, "setResultAndFinish(selectedDevice = "
- + getService().mSelectedDevice.device + ")");
- setResult(RESULT_OK,
- new Intent().putExtra(
- CompanionDeviceManager.EXTRA_DEVICE, getService().mSelectedDevice.device));
+ 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);
+
finish();
}
- 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 initUiForSelfManagedAssociation(CharSequence appLabel) {
+ if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()");
- private SparseArray<Integer> mColors = new SparseArray();
+ final CharSequence deviceName = mRequest.getDisplayName(); // "<device>";
+ final String deviceProfile = mRequest.getDeviceProfile(); // DEVICE_PROFILE_APP_STREAMING;
- private Drawable icon(int drawableRes) {
- Drawable icon = getResources().getDrawable(drawableRes, null);
- icon.setTint(Color.DKGRAY);
- return icon;
+ 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);
- @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;
- }
-
- 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));
- }
-
- 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 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;
- }
-
- @Override
- public int getCount() {
- return getService().mDevicesFound.size();
- }
-
- @Override
- public DeviceFilterPair getItem(int position) {
- return getService().mDevicesFound.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
+ mListView.setVisibility(View.GONE);
}
+
+ 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);
+ }
+
+ mTitle.setText(title);
+ mSummary.setText(summary);
+
+ mListView.setVisibility(View.GONE);
+ }
+
+ private void initUiForMultipleDevices(CharSequence appLabel) {
+ if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()");
+
+ final String deviceProfile = mRequest.getDeviceProfile();
+
+ 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 {
+ throw new RuntimeException("Unsupported profile " + deviceProfile);
+ }
+ final Spanned title = getHtmlFromResources(
+ this, R.string.chooser_title, profileName, appLabel);
+
+ mTitle.setText(title);
+ mSummary.setText(summary);
+
+ 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);
+
+ // "Remove" consent button: users would need to click on the list item.
+ mButtonAllow.setVisibility(View.GONE);
+ }
+
+ private void onListItemClick(int position) {
+ if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
+
+ final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position);
+ final MacAddress macAddress = selectedDevice.getMacAddress();
+ onAssociationApproved(macAddress);
+ }
+
+ private void onAllowButtonClick(View v) {
+ if (DEBUG) Log.d(TAG, "onAllowButtonClick()");
+
+ // Disable the button, to prevent more clicks.
+ v.setEnabled(false);
+
+ final MacAddress macAddress;
+ if (mRequest.isSelfManaged()) {
+ macAddress = null;
+ } else {
+ // TODO: implement.
+ throw new UnsupportedOperationException(
+ "isSingleDevice() requests are not supported yet.");
+ }
+ onAssociationApproved(macAddress);
+ }
+
+ private void onAssociationApproved(@Nullable MacAddress macAddress) {
+ if (mAssociationApproved) return;
+ mAssociationApproved = true;
+
+ if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress);
+
+ if (!mRequest.isSelfManaged()) {
+ requireNonNull(macAddress);
+ CompanionDeviceDiscoveryService.stop(this);
+ }
+
+ 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);
+ }
+
+ 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 126b823..a4ff1dc 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.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.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 BluetoothLeScanner mBleScanner;
- private List<DeviceFilter<?>> mFilters;
- private List<BluetoothLeDeviceFilter> mBLEFilters;
- private List<BluetoothDeviceFilter> mBluetoothFilters;
- private List<WifiDeviceFilter> mWifiFilters;
- private List<ScanFilter> mBLEScanFilters;
+ private ScanCallback mBleScanCallback;
+ private BluetoothBroadcastReceiver mBtReceiver;
+ private WifiBroadcastReceiver mWifiReceiver;
- AssociationRequest mRequest;
- List<DeviceFilterPair> mDevicesFound;
- DeviceFilterPair mSelectedDevice;
- IAssociationRequestCallback mApplicationCallback;
+ private boolean mDiscoveryStarted = false;
+ private boolean mDiscoveryStopped = false;
+ private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>();
- AndroidFuture<String> mServiceCallback;
- boolean mIsScanning = false;
- @Nullable
- CompanionDeviceActivity mActivity = null;
+ private final Runnable mTimeoutRunnable = this::timeout;
- 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));
- }
+ 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);
+ }
- @Override
- public void onAssociationCreated() {
- Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated);
- }
- };
+ static void stop(@NonNull Context context) {
+ final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
+ intent.setAction(ACTION_STOP_DISCOVERY);
+ context.startService(intent);
+ }
- private ScanCallback mBLEScanCallback;
- private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
- private WifiBroadcastReceiver mWifiBroadcastReceiver;
-
- @Override
- public IBinder onBind(Intent intent) {
- Log.i(LOG_TAG, "onBind(" + intent + ")");
- return mBinder.asBinder();
+ @MainThread
+ static @NonNull List<DeviceFilterPair<?>> getScanResults() {
+ return sInstance != null ? new ArrayList<>(sInstance.mDevicesFound)
+ : Collections.emptyList();
}
@Override
public void onCreate() {
super.onCreate();
-
- Log.i(LOG_TAG, "onCreate()");
-
- mBluetoothManager = getSystemService(BluetoothManager.class);
- mBluetoothAdapter = mBluetoothManager.getAdapter();
- mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
- mWifiManager = getSystemService(WifiManager.class);
-
- mDevicesFound = new ArrayList<>();
+ if (DEBUG) Log.d(TAG, "onCreate()");
sInstance = this;
- }
- @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);
- }
-
- if (!ArrayUtils.isEmpty(mDevicesFound)) {
- onReadyToShowUI();
- }
-
- // 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 (shouldScan(mBluetoothFilters)) {
- final IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
-
- Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
- mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
- registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
- mBluetoothAdapter.startDiscovery();
- }
-
- if (shouldScan(mBLEFilters) && mBLEScanner != null) {
- Log.i(LOG_TAG, "BLEScanner.startScan");
- mBLEScanCallback = new BLEScanCallback();
- mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
- }
-
- 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();
- }
- mIsScanning = true;
- Handler.getMain().sendMessageDelayed(
- obtainMessage(CompanionDeviceDiscoveryService::stopScan, this),
- SCAN_TIMEOUT);
- }
-
- @MainThread
- private void onAssociationCreated() {
- mActivity.setResultAndFinish();
- }
-
- private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
- return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
- }
-
- @MainThread
- private void reset() {
- Log.i(LOG_TAG, "reset()");
- stopScan();
- mDevicesFound.clear();
- mSelectedDevice = null;
- CompanionDeviceActivity.notifyDevicesChanged();
+ mBtManager = getSystemService(BluetoothManager.class);
+ mBtAdapter = mBtManager.getAdapter();
+ mBleScanner = mBtAdapter.getBluetoothLeScanner();
+ mWifiManager = getSystemService(WifiManager.class);
}
@Override
- public boolean onUnbind(Intent intent) {
- Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")");
- stopScan();
- return super.onUnbind(intent);
+ 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;
}
- private void stopScan() {
- Log.i(LOG_TAG, "stopScan()");
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (DEBUG) Log.d(TAG, "onDestroy()");
- if (!mIsScanning) return;
- mIsScanning = false;
-
- if (mActivity != null && mActivity.mDeviceListView != null) {
- mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator);
- }
-
- mBluetoothAdapter.cancelDiscovery();
- if (mBluetoothBroadcastReceiver != null) {
- unregisterReceiver(mBluetoothBroadcastReceiver);
- mBluetoothBroadcastReceiver = null;
- }
- if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
- if (mWifiBroadcastReceiver != null) {
- unregisterReceiver(mWifiBroadcastReceiver);
- mWifiBroadcastReceiver = null;
- }
- }
-
- private void onDeviceFound(@Nullable DeviceFilterPair device) {
- if (device == null) return;
-
- Handler.getMain().sendMessage(obtainMessage(
- CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device));
+ sInstance = null;
}
@MainThread
- void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) {
- if (mDevicesFound.contains(device)) {
- Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices");
- return;
- }
+ private void startDiscovery(@NonNull AssociationRequest request) {
+ if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request);
+ requireNonNull(request);
- Log.i(LOG_TAG, "Found device " + device);
+ if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress.");
+ mDiscoveryStarted = true;
- if (mDevicesFound.isEmpty()) {
- onReadyToShowUI();
- }
- mDevicesFound.add(device);
- CompanionDeviceActivity.notifyDevicesChanged();
- }
+ 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);
- //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);
- }
- }
+ checkBoundDevicesIfNeeded(request, btFilters);
- private void onDeviceLost(@Nullable DeviceFilterPair device) {
- Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
- Handler.getMain().sendMessage(obtainMessage(
- CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device));
+ // 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);
}
@MainThread
- void onDeviceLostMainThread(@Nullable DeviceFilterPair device) {
- mDevicesFound.remove(device);
- CompanionDeviceActivity.notifyDevicesChanged();
- }
+ private void stopDiscoveryAndFinish() {
+ if (DEBUG) Log.i(TAG, "stopDiscovery()");
- void onDeviceSelected(String callingPackage, String deviceAddress) {
- if (callingPackage == null || deviceAddress == null) {
+ if (!mDiscoveryStarted) {
+ stopSelf();
return;
}
- mServiceCallback.complete(deviceAddress);
- }
- void onCancel() {
- if (DEBUG) Log.i(LOG_TAG, "onCancel()");
- mActivity = null;
- mServiceCallback.cancel(true);
- }
+ if (mDiscoveryStopped) return;
+ mDiscoveryStopped = true;
- /**
- * 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;
+ // Stop BT discovery.
+ if (mBtReceiver != null) {
+ // Cancel discovery.
+ mBtAdapter.cancelDiscovery();
+ // Unregister receiver.
+ unregisterReceiver(mBtReceiver);
+ mBtReceiver = null;
}
- /**
- * {@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;
+ // Stop Wi-Fi scanning.
+ if (mWifiReceiver != null) {
+ // TODO: need to stop scan?
+ // Unregister receiver.
+ unregisterReceiver(mWifiReceiver);
+ mWifiReceiver = 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());
- }
+ // Stop BLE scanning.
+ if (mBleScanCallback != null) {
+ mBleScanner.stopScan(mBleScanCallback);
+ }
+
+ Handler.getMain().removeCallbacks(mTimeoutRunnable);
+
+ // "Finish".
+ stopSelf();
+ }
+
+ 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;
+
+ final BluetoothDeviceFilter singleMacAddressFilter =
+ find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress()));
+
+ if (singleMacAddressFilter == null) return;
+
+ findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters);
+ findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters);
+ findAndReportMatches(
+ mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters);
+ }
+
+ private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
+ @NonNull List<BluetoothDeviceFilter> filters) {
+ if (devices == null) return;
+
+ for (BluetoothDevice device : devices) {
+ final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters);
+ if (match != null) {
+ onDeviceFound(match);
}
- return filter.getDeviceDisplayName(device);
+ }
+ }
+
+ private BluetoothBroadcastReceiver startBtScanningIfNeeded(
+ List<BluetoothDeviceFilter> filters, boolean force) {
+ if (isEmpty(filters) && !force) return null;
+ if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
+
+ final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters);
+
+ final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+ registerReceiver(receiver, intentFilter);
+
+ mBtAdapter.startDiscovery();
+
+ return receiver;
+ }
+
+ 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)");
+
+ final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters);
+
+ final IntentFilter intentFilter = new IntentFilter(
+ WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ registerReceiver(receiver, intentFilter);
+
+ mWifiManager.startScan();
+
+ return receiver;
+ }
+
+ private ScanCallback startBleScanningIfNeeded(
+ List<BluetoothLeDeviceFilter> filters, boolean force) {
+ if (isEmpty(filters) && !force) return null;
+ if (DEBUG) Log.d(TAG, "BLEScanner.startScan");
+
+ if (mBleScanner == null) {
+ Log.w(TAG, "BLE Scanner is not available.");
+ return null;
}
- @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));
- }
+ final BLEScanCallback callback = new BLEScanCallback(filters);
- @Override
- public int hashCode() {
- return Objects.hash(getDeviceMacAddress(device));
- }
+ 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);
- @Override
- public String toString() {
- return "DeviceFilterPair{"
- + "device=" + device + " " + getDisplayName()
- + ", filter=" + filter
- + '}';
- }
+ 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;
+ }
+ if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
+
+ // First: make change.
+ mDevicesFound.add(device);
+ // Then: notify observers.
+ SCAN_RESULTS_OBSERVABLE.notifyObservers();
+ });
+ }
+
+ private void onDeviceLost(@Nullable DeviceFilterPair<?> device) {
+ runOnMainThread(() -> {
+ if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
+
+ // 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 0000000..faca1ae
--- /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 0000000..cf2a2bf
--- /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 0000000..eab421e
--- /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 bcc345f..637994f 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 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 @@
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 @@
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);
- }
+ // 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;
+ }
- request.setDeviceProfilePrivilegesDescription(description);
+ // 2. Create association and notify the application.
+ final AssociationInfo association = createAssociationAndNotifyApplication(
+ request, packageName, userId, macAddress, callback);
- return mServiceConnectors.forUser(userId).postAsync(service -> {
- if (DEBUG) {
- Slog.d(TAG, "Connected to CDM service -> "
- + "Starting discovery for " + request);
- }
+ // 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);
+ }
- AndroidFuture<String> future = new AndroidFuture<>();
- service.startDiscovery(request, packageName, callback, future);
- return future;
- }).cancelTimeout();
+ 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());
- }, 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();
- }));
+ try {
+ callback.onAssociationCreated(association);
+ } catch (RemoteException ignore) { }
+
+ return association;
}
private boolean willAddRoleHolder(@NonNull AssociationRequest request,
@@ -197,26 +272,44 @@
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;
- }
- mRequest = null;
- }
- }
- private boolean mayAssociateWithoutPrompt(String packageName, int userId) {
- final String deviceProfile = mRequest.getDeviceProfile();
+ if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
+ Slog.w(TAG, "Unknown result code:" + resultCode);
+ return;
+ }
+
+ 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(@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 @@
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 @@
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 @@
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);
- private interface ThrowingRunnable {
- void run() throws RemoteException;
+ final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ 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 9c996f4..626128a 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 @@
enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
"create associations");
- mAssociationRequestsProcessor.process(request, packageName, userId, callback);
+ mAssociationRequestsProcessor.processNewAssociationRequest(
+ request, packageName, userId, callback);
}
@Override
@@ -717,8 +718,6 @@
.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 ea57089..3a8ee73 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.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 @@
}
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 @@
}
}
- 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) {