Merge "Update CMD association flow."
This commit is contained in:
committed by
Android (Google) Code Review
commit
a097209fef
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Padding applied on most UI elements -->
|
||||
<dimen name="padding">12dp</dimen>
|
||||
|
||||
</resources>
|
||||
@@ -19,25 +19,58 @@
|
||||
<!-- Title of the CompanionDeviceManager application. [CHAR LIMIT=50] -->
|
||||
<string name="app_label">Companion Device Manager</string>
|
||||
|
||||
<!-- Title of the device selection dialog. -->
|
||||
<string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string>
|
||||
<!-- Title of the device association confirmation dialog. -->
|
||||
<string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
|
||||
|
||||
<!-- The generic placeholder for a device type when nothing specific is known about it [CHAR LIMIT=30] -->
|
||||
<string name="profile_name_generic">device</string>
|
||||
<!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
|
||||
|
||||
<!-- The name of the "watch" device type [CHAR LIMIT=30] -->
|
||||
<string name="profile_name_watch">watch</string>
|
||||
|
||||
<!-- Title of the device association confirmation dialog. -->
|
||||
<string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
|
||||
<!-- Title of the device selection dialog. -->
|
||||
<string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string>
|
||||
|
||||
<!-- Text of the device profile permissions explanation in the association dialog. -->
|
||||
<string name="profile_summary">This app is needed to manage your <xliff:g id="profile_name" example="watch">%1$s</xliff:g>. <xliff:g id="privileges_discplaimer" example="Android Wear will get access to your Notifications, Calendar and Contacts.">%2$s</xliff:g></string>
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
|
||||
|
||||
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
|
||||
|
||||
<!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream applications?</string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_app_streaming" product="default">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this phone when connected.</string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_app_streaming" product="tablet">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this tablet when connected.</string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_app_streaming" product="device">Let <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to provide <strong><xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g></strong> remote access to access to applications installed on this device when connected.</string>
|
||||
|
||||
<!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= -->
|
||||
|
||||
<!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="title_automotive_projection"></string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_automotive_projection"></string>
|
||||
|
||||
<!-- ================= null profile ================= -->
|
||||
|
||||
<!-- A noun for a companion device with unspecified profile (type) [CHAR LIMIT=30] -->
|
||||
<string name="profile_name_generic">device</string>
|
||||
|
||||
<!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
|
||||
<string name="summary_generic"></string>
|
||||
|
||||
<!-- ================= Buttons ================= -->
|
||||
|
||||
<!-- Positive button for the device-app association consent dialog [CHAR LIMIT=30] -->
|
||||
<string name="consent_yes">Allow</string>
|
||||
|
||||
<!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] -->
|
||||
<string name="consent_no">Don\u2019t allow</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user