Update CMD association flow.

Adapt CDM Association flow for new associate() API, which supports
self-managed associations.
Add support for new device profiles: APP_STREAMING and
AUTOMOTIVE_PROJECTION.
Start CDM discovery service only after UI is visible.
Lift "one request at a time" limitation (remove
AssociationRequestsProcessor.mRequest field).

Bug: 194301022
Bug: 208307075
Test: atest CtsCompanionDevicesTestCases:AssociateTest
Test: atest CtsCompanionDevicesTestCases:AssociateSelfManagedTest
Test: atest CtsCompanionDevicesTestCases
Change-Id: I6cb796c375421f101e87ad436111996495eb74d9
This commit is contained in:
Sergey Nikolaienkov
2021-12-02 15:47:36 +01:00
parent ed18bb120e
commit 9a2b8a1607
19 changed files with 1364 additions and 1098 deletions

View File

@@ -20,12 +20,15 @@ import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import static java.util.Objects.requireNonNull;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.annotation.UserIdInt;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
@@ -52,12 +55,11 @@ import java.util.Objects;
* device to be shown instead of a list to choose from
*/
@DataClass(
genConstructor = false,
genToString = true,
genEqualsHashCode = true,
genHiddenGetters = true,
genParcelable = true,
genHiddenConstructor = true,
genBuilder = false,
genConstDefs = false)
public final class AssociationRequest implements Parcelable {
/**
@@ -151,39 +153,75 @@ public final class AssociationRequest implements Parcelable {
private final boolean mForceConfirmation;
/**
* The app package making the request.
*
* The app package name of the application the association will belong to.
* Populated by the system.
*
* @hide
*/
private @Nullable String 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.
*/
@@ -237,8 +275,13 @@ public final class AssociationRequest implements Parcelable {
}
/** @hide */
public void setCallingPackage(@NonNull String pkg) {
mCallingPackage = pkg;
public void setPackageName(@NonNull String packageName) {
mPackageName = packageName;
}
/** @hide */
public void setUserId(@UserIdInt int userId) {
mUserId = userId;
}
/** @hide */
@@ -248,7 +291,7 @@ public final class AssociationRequest implements Parcelable {
/** @hide */
public void setSkipPrompt(boolean value) {
mSkipPrompt = true;
mSkipPrompt = value;
}
/** @hide */
@@ -258,10 +301,6 @@ public final class AssociationRequest implements Parcelable {
return mDeviceFilters;
}
private void onConstructed() {
mCreationTime = System.currentTimeMillis();
}
/**
* A builder for {@link AssociationRequest}
*/
@@ -325,7 +364,7 @@ public final class AssociationRequest implements Parcelable {
@NonNull
public Builder setDisplayName(@NonNull CharSequence displayName) {
checkNotUsed();
mDisplayName = Objects.requireNonNull(displayName);
mDisplayName = requireNonNull(displayName);
return this;
}
@@ -372,15 +411,13 @@ public final class AssociationRequest implements Parcelable {
+ "provide the display name of the device");
}
return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters),
mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation,
null, null, -1L, false);
mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation);
}
}
// Code below generated by codegen v1.0.23.
//
// DO NOT MODIFY!
@@ -395,88 +432,29 @@ public final class AssociationRequest implements Parcelable {
/**
* Creates a new AssociationRequest.
*
* @param singleDevice
* Whether only a single device should match the provided filter.
*
* When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
* address, bonded devices are also searched among. This allows to obtain the necessary app
* privileges even if the device is already paired.
* @param deviceFilters
* If set, only devices matching either of the given filters will be shown to the user
* @param deviceProfile
* Profile of the device.
* @param displayName
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
* @param selfManaged
* Whether the association is to be managed by the companion application.
* @param forceConfirmation
* Indicates that the application would prefer the CompanionDeviceManager to collect an explicit
* confirmation from the user before creating an association, even if such confirmation is not
* required.
* @param callingPackage
* The app package making the request.
*
* Populated by the system.
* @param deviceProfilePrivilegesDescription
* The user-readable description of the device profile's privileges.
*
* Populated by the system.
* @param creationTime
* The time at which his request was created
* @param skipPrompt
* Whether the user-prompt may be skipped once the device is found.
*
* 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 @@ public final class AssociationRequest implements Parcelable {
/**
* Whether the user-prompt may be skipped once the device is found.
*
* Populated by the system.
*
* @hide
@@ -521,7 +498,8 @@ public final class AssociationRequest implements Parcelable {
"displayName = " + mDisplayName + ", " +
"selfManaged = " + mSelfManaged + ", " +
"forceConfirmation = " + mForceConfirmation + ", " +
"callingPackage = " + mCallingPackage + ", " +
"packageName = " + mPackageName + ", " +
"userId = " + mUserId + ", " +
"deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " +
"creationTime = " + mCreationTime + ", " +
"skipPrompt = " + mSkipPrompt +
@@ -547,7 +525,8 @@ public final class AssociationRequest implements Parcelable {
&& Objects.equals(mDisplayName, that.mDisplayName)
&& mSelfManaged == that.mSelfManaged
&& mForceConfirmation == that.mForceConfirmation
&& Objects.equals(mCallingPackage, that.mCallingPackage)
&& Objects.equals(mPackageName, that.mPackageName)
&& mUserId == that.mUserId
&& Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription)
&& mCreationTime == that.mCreationTime
&& mSkipPrompt == that.mSkipPrompt;
@@ -566,7 +545,8 @@ public final class AssociationRequest implements Parcelable {
_hash = 31 * _hash + Objects.hashCode(mDisplayName);
_hash = 31 * _hash + Boolean.hashCode(mSelfManaged);
_hash = 31 * _hash + Boolean.hashCode(mForceConfirmation);
_hash = 31 * _hash + Objects.hashCode(mCallingPackage);
_hash = 31 * _hash + Objects.hashCode(mPackageName);
_hash = 31 * _hash + mUserId;
_hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription);
_hash = 31 * _hash + Long.hashCode(mCreationTime);
_hash = 31 * _hash + Boolean.hashCode(mSkipPrompt);
@@ -583,16 +563,17 @@ public final class AssociationRequest implements Parcelable {
if (mSingleDevice) flg |= 0x1;
if (mSelfManaged) flg |= 0x10;
if (mForceConfirmation) flg |= 0x20;
if (mSkipPrompt) flg |= 0x200;
if (mSkipPrompt) flg |= 0x400;
if (mDeviceProfile != null) flg |= 0x4;
if (mDisplayName != null) flg |= 0x8;
if (mCallingPackage != null) flg |= 0x40;
if (mDeviceProfilePrivilegesDescription != null) flg |= 0x80;
if (mPackageName != null) flg |= 0x40;
if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100;
dest.writeInt(flg);
dest.writeParcelableList(mDeviceFilters, flags);
if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
if (mDisplayName != null) dest.writeCharSequence(mDisplayName);
if (mCallingPackage != null) dest.writeString(mCallingPackage);
if (mPackageName != null) dest.writeString(mPackageName);
dest.writeInt(mUserId);
if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription);
dest.writeLong(mCreationTime);
}
@@ -612,13 +593,14 @@ public final class AssociationRequest implements Parcelable {
boolean singleDevice = (flg & 0x1) != 0;
boolean selfManaged = (flg & 0x10) != 0;
boolean forceConfirmation = (flg & 0x20) != 0;
boolean skipPrompt = (flg & 0x200) != 0;
boolean skipPrompt = (flg & 0x400) != 0;
List<DeviceFilter<?>> deviceFilters = new ArrayList<>();
in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader());
String deviceProfile = (flg & 0x4) == 0 ? null : in.readString();
CharSequence displayName = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence();
String callingPackage = (flg & 0x40) == 0 ? null : in.readString();
String deviceProfilePrivilegesDescription = (flg & 0x80) == 0 ? null : in.readString();
String packageName = (flg & 0x40) == 0 ? null : in.readString();
int userId = in.readInt();
String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString();
long creationTime = in.readLong();
this.mSingleDevice = singleDevice;
@@ -631,12 +613,15 @@ public final class AssociationRequest implements Parcelable {
this.mDisplayName = displayName;
this.mSelfManaged = selfManaged;
this.mForceConfirmation = forceConfirmation;
this.mCallingPackage = callingPackage;
this.mPackageName = packageName;
this.mUserId = userId;
com.android.internal.util.AnnotationValidations.validate(
UserIdInt.class, null, mUserId);
this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
this.mCreationTime = creationTime;
this.mSkipPrompt = skipPrompt;
onConstructed();
// onConstructed(); // You can define this method to get a callback
}
@DataClass.Generated.Member
@@ -654,10 +639,10 @@ public final class AssociationRequest implements Parcelable {
};
@DataClass.Generated(
time = 1638368698639L,
time = 1638962248060L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java",
inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final boolean mSelfManaged\nprivate final boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate long mCreationTime\nprivate boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic boolean isSingleDevice()\npublic void setCallingPackage(java.lang.String)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nprivate void onConstructed()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate boolean mSelfManaged\nprivate boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false, genConstDefs=false)")
inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final boolean mSelfManaged\nprivate final boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mPackageName\nprivate @android.annotation.UserIdInt int mUserId\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate final long mCreationTime\nprivate boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic boolean isSingleDevice()\npublic void setPackageName(java.lang.String)\npublic void setUserId(int)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate boolean mSelfManaged\nprivate boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genConstDefs=false)")
@Deprecated
private void __metadata() {}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
/>

View File

@@ -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"
/>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Padding applied on most UI elements -->
<dimen name="padding">12dp</dimen>
</resources>

View File

@@ -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 &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
<!-- Title of the device association confirmation dialog. -->
<string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</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 &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</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 &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</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 &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; 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 &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; 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 &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; 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 &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; 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>

View File

@@ -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>

View File

@@ -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);
}
};
}

View File

@@ -16,18 +16,17 @@
package com.android.companiondevicemanager;
import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
import static com.android.companiondevicemanager.Utils.runOnMainThread;
import static com.android.internal.util.ArrayUtils.isEmpty;
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import static com.android.internal.util.CollectionUtils.size;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.CollectionUtils.find;
import static com.android.internal.util.CollectionUtils.map;
import static java.util.Objects.requireNonNull;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -42,8 +41,6 @@ import android.companion.AssociationRequest;
import android.companion.BluetoothDeviceFilter;
import android.companion.BluetoothLeDeviceFilter;
import android.companion.DeviceFilter;
import android.companion.IAssociationRequestCallback;
import android.companion.ICompanionDeviceDiscoveryService;
import android.companion.WifiDeviceFilter;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -53,417 +50,411 @@ import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Observable;
public class CompanionDeviceDiscoveryService extends Service {
private static final boolean DEBUG = false;
private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
private static final String TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
private static final long SCAN_TIMEOUT = 20000;
private static final String ACTION_START_DISCOVERY =
"com.android.companiondevicemanager.action.START_DISCOVERY";
private static final String ACTION_STOP_DISCOVERY =
"com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY";
private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
static CompanionDeviceDiscoveryService sInstance;
private static final long SCAN_TIMEOUT = 20_000L; // 20 seconds
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
// TODO: replace with LiveData-s?
static final Observable TIMEOUT_OBSERVABLE = new MyObservable();
static final Observable SCAN_RESULTS_OBSERVABLE = new MyObservable();
private static CompanionDeviceDiscoveryService sInstance;
private BluetoothManager mBtManager;
private BluetoothAdapter mBtAdapter;
private WifiManager mWifiManager;
@Nullable private BluetoothLeScanner mBLEScanner;
private ScanSettings mDefaultScanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
private 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();
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}

View File

@@ -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() {
}
}

View File

@@ -16,8 +16,13 @@
package com.android.server.companion;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
import static android.content.ComponentName.createRelative;
import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
@@ -28,73 +33,114 @@ import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.companion.IAssociationRequestCallback;
import android.companion.ICompanionDeviceDiscoveryService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
import android.net.MacAddress;
import android.os.Binder;
import android.os.IBinder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.PackageUtils;
import android.util.Slog;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.PerUser;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.util.ArrayUtils;
import com.android.server.FgThread;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* Class responsible for handling incoming {@link AssociationRequest}s.
* The main responsibilities of an {@link AssociationRequestsProcessor} are:
* <ul>
* <li> Requests validation and checking if the package that would own the association holds all
* necessary permissions.
* <li> Communication with the requester via a provided
* {@link android.companion.CompanionDeviceManager.Callback}.
* <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
* approval.
* <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
* request was found valid and was approved by user.
* </ul>
*
* The class supports two variants of the "Association Flow": the full variant, and the shortened
* (a.k.a. No-UI) variant.
* Both flows start similarly: in
* {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
* invoked from
* {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
* method call.
* Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
* required.
*
* If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
* {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}
* which after calling to {@link CompanionDeviceManagerService} to create an association, notifies
* the requester via
* {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
*
* If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
* {@link PendingIntent} for the approval UI and sends it back to the requester via
* {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
* When/if user approves the request, {@link AssociationRequestsProcessor} receives a "callback"
* from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
* {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
* which one more time checks that the packages holds all necessary permissions before proceeding to
* {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}.
*
* @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
* @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
* ResultReceiver, MacAddress)
*/
class AssociationRequestsProcessor {
private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor";
private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
".CompanionDeviceDiscoveryService");
private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
// AssociationRequestsProcessor <-> UI
private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
// AssociationRequestsProcessor -> UI
private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
private static final String EXTRA_ASSOCIATION = "association";
// UI -> AssociationRequestsProcessor
private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
private static final String EXTRA_MAC_ADDRESS = "mac_address";
private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
private final Context mContext;
private final CompanionDeviceManagerService mService;
private final PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
private AssociationRequest mRequest;
private IAssociationRequestCallback mAppCallback;
private AndroidFuture<?> mOngoingDeviceDiscovery;
private final PackageManagerInternal mPackageManager;
AssociationRequestsProcessor(CompanionDeviceManagerService service) {
mContext = service.getContext();
mService = service;
final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
mServiceConnectors = new PerUser<>() {
@Override
protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
return new ServiceConnector.Impl<>(
mContext,
serviceIntent, 0/* bindingFlags */, userId,
ICompanionDeviceDiscoveryService.Stub::asInterface);
}
};
mPackageManager = service.mPackageManagerInternal;
}
/**
* Handle incoming {@link AssociationRequest}s, sent via
* {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
*/
void process(@NonNull AssociationRequest request, @NonNull String packageName,
@UserIdInt int userId, @NonNull IAssociationRequestCallback callback) {
void processNewAssociationRequest(@NonNull AssociationRequest request,
@NonNull String packageName, @UserIdInt int userId,
@NonNull IAssociationRequestCallback callback) {
requireNonNull(request, "Request MUST NOT be null");
if (request.isSelfManaged()) {
requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
@@ -103,14 +149,15 @@ class AssociationRequestsProcessor {
requireNonNull(packageName, "Package name MUST NOT be null");
requireNonNull(callback, "Callback MUST NOT be null");
final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
if (DEBUG) {
Slog.d(TAG, "process() "
Slog.d(TAG, "processNewAssociationRequest() "
+ "request=" + request + ", "
+ "package=u" + userId + "/" + packageName);
+ "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
}
// 1. Enforce permissions and other requirements.
enforcePermissionsForAssociation(mContext, request, packageName, userId);
enforcePermissionsForAssociation(mContext, request, packageUid);
mService.checkUsesFeature(packageName, userId);
// 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
@@ -118,71 +165,99 @@ class AssociationRequestsProcessor {
if (request.isSelfManaged() && !request.isForceConfirmation()
&& !willAddRoleHolder(request, packageName, userId)) {
// 2a. Create association right away.
final AssociationInfo association = mService.createAssociation(userId, packageName,
/* macAddress */ null, request.getDisplayName(), request.getDeviceProfile(),
/* selfManaged */true);
withCatchingRemoteException(() -> callback.onAssociationCreated(association));
createAssociationAndNotifyApplication(request, packageName, userId,
/*macAddress*/ null, callback);
return;
}
// 2b. Launch the UI.
synchronized (mService.mLock) {
if (mRequest != null) {
Slog.w(TAG, "CDM is already processing another AssociationRequest.");
// 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
withCatchingRemoteException(() -> callback.onFailure("Busy."));
}
// 2b.1. Populate the request with required info.
request.setPackageName(packageName);
request.setUserId(userId);
request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId));
final boolean linked = withCatchingRemoteException(
() -> callback.asBinder().linkToDeath(mBinderDeathRecipient, 0));
if (!linked) {
// The process has died by now: do not proceed.
return;
}
// 2b.2. Prepare extras and create an Intent.
final Bundle extras = new Bundle();
extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
mRequest = request;
final Intent intent = new Intent();
intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
intent.putExtras(extras);
// 2b.3. Create a PendingIntent.
final PendingIntent pendingIntent;
final long token = Binder.clearCallingIdentity();
try {
// Using uid of the application that will own the association (usually the same
// application that sent the request) allows us to have multiple "pending" association
// requests at the same time.
// If the application already has a pending association request, that PendingIntent
// will be cancelled.
pendingIntent = PendingIntent.getActivity(mContext, /*requestCode */ packageUid, intent,
FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
} finally {
Binder.restoreCallingIdentity(token);
}
mAppCallback = callback;
request.setCallingPackage(packageName);
// 2b.4. Send the PendingIntent back to the app.
try {
callback.onAssociationPending(pendingIntent);
} catch (RemoteException ignore) { }
}
if (mayAssociateWithoutPrompt(packageName, userId)) {
Slog.i(TAG, "setSkipPrompt(true)");
request.setSkipPrompt(true);
private void processAssociationRequestApproval(@NonNull AssociationRequest request,
@NonNull IAssociationRequestCallback callback,
@NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
final String packageName = request.getPackageName();
final int userId = request.getUserId();
final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
if (DEBUG) {
Slog.d(TAG, "processAssociationRequestApproval()\n"
+ " package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
+ " request=" + request + "\n"
+ " macAddress=" + macAddress + "\n");
}
final String deviceProfile = request.getDeviceProfile();
mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
.thenComposeAsync(description -> {
if (DEBUG) {
Slog.d(TAG, "fetchProfileDescription done: " + description);
}
// 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 @@ class AssociationRequestsProcessor {
return !isRoleHolder;
}
private void cleanup() {
if (DEBUG) {
Slog.d(TAG, "cleanup(); discovery = "
+ mOngoingDeviceDiscovery + ", request = " + mRequest);
}
synchronized (mService.mLock) {
AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
ongoingDeviceDiscovery.cancel(true);
private final ResultReceiver mOnRequestConfirmationReceiver =
new ResultReceiver(Handler.getMain()) {
@Override
protected void onReceiveResult(int resultCode, Bundle data) {
if (DEBUG) {
Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
+ "code=" + resultCode + ", " + "data=" + data);
}
if (mAppCallback != null) {
mAppCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0);
mAppCallback = null;
}
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 @@ class AssociationRequestsProcessor {
String[] sameOemCerts = mContext.getResources()
.getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
Signature[] signatures = mService.mPackageManagerInternal
.getPackage(packageName).getSigningDetails().getSignatures();
Signature[] signatures = mPackageManager.getPackage(packageName).getSigningDetails()
.getSignatures();
String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
Set<String> sameOemPackageCerts =
@@ -274,47 +367,6 @@ class AssociationRequestsProcessor {
return false;
}
@NonNull
private AndroidFuture<String> getDeviceProfilePermissionDescription(
@Nullable String deviceProfile) {
if (deviceProfile == null) {
return AndroidFuture.completedFuture(null);
}
final AndroidFuture<String> result = new AndroidFuture<>();
mService.mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
deviceProfile, FgThread.getExecutor(), desc -> {
try {
result.complete(String.valueOf(desc));
} catch (Exception e) {
result.completeExceptionally(e);
}
});
return result;
}
void dump(@NonNull PrintWriter pw) {
pw.append("Discovery Service State:").append('\n');
for (int i = 0, size = mServiceConnectors.size(); i < size; i++) {
int userId = mServiceConnectors.keyAt(i);
pw.append(" ")
.append("u").append(Integer.toString(userId)).append(": ")
.append(Objects.toString(mServiceConnectors.valueAt(i)))
.append('\n');
}
}
private final IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (DEBUG) {
Slog.d(TAG, "binderDied()");
}
mService.mMainHandler.post(AssociationRequestsProcessor.this::cleanup);
}
};
private static Set<String> getSameOemPackageCerts(
String packageName, String[] oemPackages, String[] sameOemCerts) {
Set<String> sameOemPackageCerts = new HashSet<>();
@@ -330,16 +382,19 @@ class AssociationRequestsProcessor {
return sameOemPackageCerts;
}
private static boolean withCatchingRemoteException(ThrowingRunnable runnable) {
try {
runnable.run();
} catch (RemoteException e) {
return false;
}
return true;
}
/**
* Convert an instance of a "locally-defined" ResultReceiver to an instance of
* {@link android.os.ResultReceiver} itself, which the receiving process will be able to
* unmarshall.
*/
private static <T extends ResultReceiver> ResultReceiver prepareForIpc(T resultReceiver) {
final Parcel parcel = Parcel.obtain();
resultReceiver.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
private interface ThrowingRunnable {
void run() throws RemoteException;
final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
parcel.recycle();
return ipcFriendly;
}
}

View File

@@ -406,7 +406,8 @@ public class CompanionDeviceManagerService extends SystemService {
enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
"create associations");
mAssociationRequestsProcessor.process(request, packageName, userId, callback);
mAssociationRequestsProcessor.processNewAssociationRequest(
request, packageName, userId, callback);
}
@Override
@@ -717,8 +718,6 @@ public class CompanionDeviceManagerService extends SystemService {
.append(sDateFormat.format(time)).append('\n');
}
mAssociationRequestsProcessor.dump(fout);
fout.append("Device Listener Services State:").append('\n');
for (int i = 0, size = mCompanionDevicePresenceController.mBoundServices.size();
i < size; i++) {

View File

@@ -38,13 +38,11 @@ import android.annotation.UserIdInt;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArrayMap;
import com.android.internal.app.IAppOpsService;
import com.android.server.LocalServices;
import java.util.Map;
@@ -69,9 +67,7 @@ final class PermissionsUtils {
}
static void enforcePermissionsForAssociation(@NonNull Context context,
@NonNull AssociationRequest request, @NonNull String packageName,
@UserIdInt int userId) {
final int packageUid = getPackageUid(userId, packageName);
@NonNull AssociationRequest request, int packageUid) {
enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile(), packageUid);
if (request.isSelfManaged()) {
@@ -207,11 +203,6 @@ final class PermissionsUtils {
}
}
private static int getPackageUid(@UserIdInt int userId, @NonNull String packageName) {
return LocalServices.getService(PackageManagerInternal.class)
.getPackageUid(packageName, 0, userId);
}
private static IAppOpsService getAppOpsService() {
if (sAppOpsService == null) {
synchronized (PermissionsUtils.class) {