CompanionDeviceManager

This introduces an API for apps that support companion devices to provide a
more streamlined flow for pairing and setting up the device

Bug: 30932767
Test: Using a toy app, invoke the newly introduced API (CompanionDeviceManager),
 and go through the flow. Ensure filtering works, and device is returned to
 the calling app. Ensure the calling app can pair to the selected device.
Change-Id: I0aeb653afd65e4adead13ea9c7248ec20971b04a
diff --git a/Android.mk b/Android.mk
index 1da4783..a1e9ed9 100644
--- a/Android.mk
+++ b/Android.mk
@@ -295,6 +295,10 @@
 	core/java/android/print/IWriteResultCallback.aidl \
 	core/java/android/printservice/IPrintService.aidl \
 	core/java/android/printservice/IPrintServiceClient.aidl \
+	core/java/android/companion/ICompanionDeviceManager.aidl \
+	core/java/android/companion/ICompanionDeviceManagerService.aidl \
+	core/java/android/companion/ICompanionDeviceManagerServiceCallback.aidl \
+	core/java/android/companion/IOnAssociateCallback.aidl \
 	core/java/android/service/dreams/IDreamManager.aidl \
 	core/java/android/service/dreams/IDreamService.aidl \
 	core/java/android/service/persistentdata/IPersistentDataBlockService.aidl \
diff --git a/api/current.txt b/api/current.txt
index e315958..0f0c16e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -7950,6 +7950,65 @@
 
 }
 
+package android.companion {
+
+  public final class AssociationRequest<F extends android.companion.DeviceFilter> implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.AssociationRequest> CREATOR;
+  }
+
+  public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> {
+    method public android.companion.AssociationRequest<F> build();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice();
+    method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F);
+    method public android.companion.AssociationRequest.Builder<F> setSingleDevice(boolean);
+  }
+
+  public final class BluetoothDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothDeviceFilter.Builder {
+    ctor public BluetoothDeviceFilter.Builder();
+    method public android.companion.BluetoothDeviceFilter.Builder addServiceUuid(android.os.ParcelUuid, android.os.ParcelUuid);
+    method public android.companion.BluetoothDeviceFilter build();
+    method public android.companion.BluetoothDeviceFilter.Builder setAddress(java.lang.String);
+    method public android.companion.BluetoothDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+  }
+
+  public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothLEDeviceFilter.Builder {
+    ctor public BluetoothLEDeviceFilter.Builder();
+    method public android.companion.BluetoothLEDeviceFilter build();
+    method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+    method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter);
+  }
+
+  public final class CompanionDeviceManager {
+    method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
+    field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
+  }
+
+  public static abstract class CompanionDeviceManager.Callback {
+    ctor public CompanionDeviceManager.Callback();
+    method public abstract void onDeviceFound(android.content.IntentSender);
+    method public abstract void onFailure(java.lang.CharSequence);
+  }
+
+  public abstract interface DeviceFilter<D extends android.os.Parcelable> implements android.os.Parcelable {
+  }
+
+}
+
 package android.content {
 
   public abstract class AbstractThreadedSyncAdapter {
@@ -8562,6 +8621,7 @@
     field public static final java.lang.String CAPTIONING_SERVICE = "captioning";
     field public static final java.lang.String CARRIER_CONFIG_SERVICE = "carrier_config";
     field public static final java.lang.String CLIPBOARD_SERVICE = "clipboard";
+    field public static final java.lang.String COMPANION_DEVICE_SERVICE = "companion_device";
     field public static final java.lang.String CONNECTIVITY_SERVICE = "connectivity";
     field public static final java.lang.String CONSUMER_IR_SERVICE = "consumer_ir";
     field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2
diff --git a/api/system-current.txt b/api/system-current.txt
index ae145dc..50d61ae 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8340,6 +8340,65 @@
 
 }
 
+package android.companion {
+
+  public final class AssociationRequest<F extends android.companion.DeviceFilter> implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.AssociationRequest> CREATOR;
+  }
+
+  public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> {
+    method public android.companion.AssociationRequest<F> build();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice();
+    method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F);
+    method public android.companion.AssociationRequest.Builder<F> setSingleDevice(boolean);
+  }
+
+  public final class BluetoothDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothDeviceFilter.Builder {
+    ctor public BluetoothDeviceFilter.Builder();
+    method public android.companion.BluetoothDeviceFilter.Builder addServiceUuid(android.os.ParcelUuid, android.os.ParcelUuid);
+    method public android.companion.BluetoothDeviceFilter build();
+    method public android.companion.BluetoothDeviceFilter.Builder setAddress(java.lang.String);
+    method public android.companion.BluetoothDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+  }
+
+  public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothLEDeviceFilter.Builder {
+    ctor public BluetoothLEDeviceFilter.Builder();
+    method public android.companion.BluetoothLEDeviceFilter build();
+    method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+    method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter);
+  }
+
+  public final class CompanionDeviceManager {
+    method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
+    field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
+  }
+
+  public static abstract class CompanionDeviceManager.Callback {
+    ctor public CompanionDeviceManager.Callback();
+    method public abstract void onDeviceFound(android.content.IntentSender);
+    method public abstract void onFailure(java.lang.CharSequence);
+  }
+
+  public abstract interface DeviceFilter<D extends android.os.Parcelable> implements android.os.Parcelable {
+  }
+
+}
+
 package android.content {
 
   public abstract class AbstractThreadedSyncAdapter {
@@ -8959,6 +9018,7 @@
     field public static final java.lang.String CAPTIONING_SERVICE = "captioning";
     field public static final java.lang.String CARRIER_CONFIG_SERVICE = "carrier_config";
     field public static final java.lang.String CLIPBOARD_SERVICE = "clipboard";
+    field public static final java.lang.String COMPANION_DEVICE_SERVICE = "companion_device";
     field public static final java.lang.String CONNECTIVITY_SERVICE = "connectivity";
     field public static final java.lang.String CONSUMER_IR_SERVICE = "consumer_ir";
     field public static final java.lang.String CONTEXTHUB_SERVICE = "contexthub";
diff --git a/api/test-current.txt b/api/test-current.txt
index 1e3b6db..73e8050 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -7973,6 +7973,65 @@
 
 }
 
+package android.companion {
+
+  public final class AssociationRequest<F extends android.companion.DeviceFilter> implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.AssociationRequest> CREATOR;
+  }
+
+  public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> {
+    method public android.companion.AssociationRequest<F> build();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice();
+    method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice();
+    method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F);
+    method public android.companion.AssociationRequest.Builder<F> setSingleDevice(boolean);
+  }
+
+  public final class BluetoothDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothDeviceFilter.Builder {
+    ctor public BluetoothDeviceFilter.Builder();
+    method public android.companion.BluetoothDeviceFilter.Builder addServiceUuid(android.os.ParcelUuid, android.os.ParcelUuid);
+    method public android.companion.BluetoothDeviceFilter build();
+    method public android.companion.BluetoothDeviceFilter.Builder setAddress(java.lang.String);
+    method public android.companion.BluetoothDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+  }
+
+  public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR;
+  }
+
+  public static final class BluetoothLEDeviceFilter.Builder {
+    ctor public BluetoothLEDeviceFilter.Builder();
+    method public android.companion.BluetoothLEDeviceFilter build();
+    method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern);
+    method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter);
+  }
+
+  public final class CompanionDeviceManager {
+    method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
+    field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
+  }
+
+  public static abstract class CompanionDeviceManager.Callback {
+    ctor public CompanionDeviceManager.Callback();
+    method public abstract void onDeviceFound(android.content.IntentSender);
+    method public abstract void onFailure(java.lang.CharSequence);
+  }
+
+  public abstract interface DeviceFilter<D extends android.os.Parcelable> implements android.os.Parcelable {
+  }
+
+}
+
 package android.content {
 
   public abstract class AbstractThreadedSyncAdapter {
@@ -8587,6 +8646,7 @@
     field public static final java.lang.String CAPTIONING_SERVICE = "captioning";
     field public static final java.lang.String CARRIER_CONFIG_SERVICE = "carrier_config";
     field public static final java.lang.String CLIPBOARD_SERVICE = "clipboard";
+    field public static final java.lang.String COMPANION_DEVICE_SERVICE = "companion_device";
     field public static final java.lang.String CONNECTIVITY_SERVICE = "connectivity";
     field public static final java.lang.String CONSUMER_IR_SERVICE = "consumer_ir";
     field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index fc1d613..44db326 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -30,6 +30,8 @@
 import android.app.usage.UsageStatsManager;
 import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothManager;
+import android.companion.CompanionDeviceManager;
+import android.companion.ICompanionDeviceManager;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.IRestrictionsManager;
@@ -634,6 +636,18 @@
                         UserHandle.getAppId(Process.myUid()));
             }});
 
+        registerService(Context.COMPANION_DEVICE_SERVICE, CompanionDeviceManager.class,
+                new CachedServiceFetcher<CompanionDeviceManager>() {
+                    @Override
+                    public CompanionDeviceManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder iBinder =
+                                ServiceManager.getServiceOrThrow(Context.COMPANION_DEVICE_SERVICE);
+                        ICompanionDeviceManager service =
+                                ICompanionDeviceManager.Stub.asInterface(iBinder);
+                        return new CompanionDeviceManager(service, ctx);
+                    }});
+
         registerService(Context.CONSUMER_IR_SERVICE, ConsumerIrManager.class,
                 new CachedServiceFetcher<ConsumerIrManager>() {
             @Override
diff --git a/core/java/android/bluetooth/le/ScanFilter.java b/core/java/android/bluetooth/le/ScanFilter.java
index 17a802d..b89c64a 100644
--- a/core/java/android/bluetooth/le/ScanFilter.java
+++ b/core/java/android/bluetooth/le/ScanFilter.java
@@ -67,7 +67,9 @@
     private final byte[] mManufacturerData;
     @Nullable
     private final byte[] mManufacturerDataMask;
-    private static final ScanFilter EMPTY = new ScanFilter.Builder().build() ;
+
+    /** @hide */
+    public static final ScanFilter EMPTY = new ScanFilter.Builder().build() ;
 
 
     private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
@@ -318,8 +320,12 @@
         return true;
     }
 
-    // Check if the uuid pattern is contained in a list of parcel uuids.
-    private boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask,
+    /**
+     * Check if the uuid pattern is contained in a list of parcel uuids.
+     *
+     * @hide
+     */
+    public static boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask,
             List<ParcelUuid> uuids) {
         if (uuid == null) {
             return true;
@@ -338,7 +344,7 @@
     }
 
     // Check if the uuid pattern matches the particular service uuid.
-    private boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
+    private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
         if (mask == null) {
             return uuid.equals(data);
         }
diff --git a/core/java/android/companion/AssociationRequest.aidl b/core/java/android/companion/AssociationRequest.aidl
new file mode 100644
index 0000000..6c91062
--- /dev/null
+++ b/core/java/android/companion/AssociationRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+parcelable AssociationRequest;
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
new file mode 100644
index 0000000..d477f43
--- /dev/null
+++ b/core/java/android/companion/AssociationRequest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.OneTimeUseBuilder;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A request for the user to select a companion device to associate with.
+ *
+ * You can optionally set a {@link Builder#setDeviceFilter filter} for which devices to show to the
+ * user to select from.
+ * The exact type and fields of the filter you can set depend on the
+ * medium type. See {@link Builder}'s static factory methods for specific protocols that are
+ * supported.
+ *
+ * You can also set {@link Builder#setSingleDevice single device} to request a popup with single
+ * device to be shown instead of a list to choose from
+ *
+ * @param <F> Device filter type
+ */
+public final class AssociationRequest<F extends DeviceFilter> implements Parcelable {
+
+    /** @hide */
+    public static final int MEDIUM_TYPE_BLUETOOTH = 0;
+    /** @hide */
+    public static final int MEDIUM_TYPE_BLUETOOTH_LE = 1;
+    /** @hide */
+    public static final int MEDIUM_TYPE_WIFI = 2;
+
+    /** @hide */
+    @IntDef({MEDIUM_TYPE_BLUETOOTH, MEDIUM_TYPE_BLUETOOTH_LE, MEDIUM_TYPE_WIFI})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediumType {}
+
+    private final boolean mSingleDevice;
+    private final int mMediumType;
+    private final F mDeviceFilter;
+
+    private AssociationRequest(boolean singleDevice, int mMediumType, F deviceFilter) {
+        this.mSingleDevice = singleDevice;
+        this.mMediumType = mMediumType;
+        this.mDeviceFilter = deviceFilter;
+    }
+
+    private AssociationRequest(Parcel in) {
+        this(
+            in.readByte() != 0,
+            in.readInt(),
+            in.readParcelable(AssociationRequest.class.getClassLoader()));
+    }
+
+    /** @hide */
+    public boolean isSingleDevice() {
+        return mSingleDevice;
+    }
+
+    /** @hide */
+    @MediumType
+    public int getMediumType() {
+        return mMediumType;
+    }
+
+    /** @hide */
+    @Nullable
+    public F getDeviceFilter() {
+        return mDeviceFilter;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByte((byte) (mSingleDevice ? 1 : 0));
+        dest.writeInt(mMediumType);
+        dest.writeParcelable(mDeviceFilter, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<AssociationRequest> CREATOR = new Creator<AssociationRequest>() {
+        @Override
+        public AssociationRequest createFromParcel(Parcel in) {
+            return new AssociationRequest(in);
+        }
+
+        @Override
+        public AssociationRequest[] newArray(int size) {
+            return new AssociationRequest[size];
+        }
+    };
+
+    /**
+     * A builder for {@link AssociationRequest}
+     *
+     * @param <F> the type of filter for the request.
+     */
+    public static final class Builder<F extends DeviceFilter>
+            extends OneTimeUseBuilder<AssociationRequest<F>> {
+        private boolean mSingleDevice = false;
+        @MediumType private int mMediumType;
+        @Nullable private F mDeviceFilter = null;
+
+        private Builder() {}
+
+        /**
+         * Create a new builder for an association request with a Bluetooth LE device
+         */
+        @NonNull
+        public static Builder<BluetoothLEDeviceFilter> createForBluetoothLEDevice() {
+            return new Builder<BluetoothLEDeviceFilter>()
+                    .setMediumType(MEDIUM_TYPE_BLUETOOTH_LE);
+        }
+
+        /**
+         * Create a new builder for an association request with a Bluetooth(non-LE) device
+         */
+        @NonNull
+        public static Builder<BluetoothDeviceFilter> createForBluetoothDevice() {
+            return new Builder<BluetoothDeviceFilter>()
+                    .setMediumType(MEDIUM_TYPE_BLUETOOTH);
+        }
+
+        //TODO implement, once specific filter classes are available
+//        public static Builder<> createForWiFiDevice()
+//        public static Builder<> createForNanDevice()
+
+        /**
+         * @param singleDevice if true, scanning for a device will stop as soon as at least one
+         *                     fitting device is found
+         */
+        @NonNull
+        public Builder<F> setSingleDevice(boolean singleDevice) {
+            checkNotUsed();
+            this.mSingleDevice = singleDevice;
+            return this;
+        }
+
+        /**
+         * @param deviceFilter if set, only devices matching the given filter will be shown to the
+         *                     user
+         */
+        @NonNull
+        public Builder<F> setDeviceFilter(@Nullable F deviceFilter) {
+            checkNotUsed();
+            this.mDeviceFilter = deviceFilter;
+            return this;
+        }
+
+        /**
+         * @param deviceType A type of medium over which to discover devices
+         *
+         * @see MediumType
+         */
+        @NonNull
+        private Builder<F> setMediumType(@MediumType int deviceType) {
+            mMediumType = deviceType;
+            return this;
+        }
+
+        /** @inheritDoc */
+        @NonNull
+        @Override
+        public AssociationRequest<F> build() {
+            markUsed();
+            return new AssociationRequest<>(mSingleDevice, mMediumType, mDeviceFilter);
+        }
+    }
+}
diff --git a/core/java/android/companion/BluetoothDeviceFilter.java b/core/java/android/companion/BluetoothDeviceFilter.java
new file mode 100644
index 0000000..5a69955
--- /dev/null
+++ b/core/java/android/companion/BluetoothDeviceFilter.java
@@ -0,0 +1,202 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+import static android.companion.BluetoothDeviceFilterUtils.matchesAddress;
+import static android.companion.BluetoothDeviceFilterUtils.matchesName;
+import static android.companion.BluetoothDeviceFilterUtils.matchesServiceUuids;
+import static android.companion.BluetoothDeviceFilterUtils.patternFromString;
+import static android.companion.BluetoothDeviceFilterUtils.patternToString;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.provider.OneTimeUseBuilder;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * A filter for Bluetooth(non-LE) devices
+ */
+public final class BluetoothDeviceFilter implements DeviceFilter<BluetoothDevice> {
+
+    private static BluetoothDeviceFilter NO_OP;
+
+    private final Pattern mNamePattern;
+    private final String mAddress;
+    private final List<ParcelUuid> mServiceUuids;
+    private final List<ParcelUuid> mServiceUuidMasks;
+
+    private BluetoothDeviceFilter(
+            Pattern namePattern,
+            String address,
+            List<ParcelUuid> serviceUuids,
+            List<ParcelUuid> serviceUuidMasks) {
+        mNamePattern = namePattern;
+        mAddress = address;
+        mServiceUuids = ArrayUtils.emptyIfNull(serviceUuids);
+        mServiceUuidMasks = ArrayUtils.emptyIfNull(serviceUuidMasks);
+    }
+
+    private BluetoothDeviceFilter(Parcel in) {
+        this(
+            patternFromString(in.readString()),
+            in.readString(),
+            readUuids(in),
+            readUuids(in));
+    }
+
+    private static List<ParcelUuid> readUuids(Parcel in) {
+        final ArrayList<ParcelUuid> list = new ArrayList<>();
+        in.readParcelableList(list, ParcelUuid.class.getClassLoader());
+        return list;
+    }
+
+    /** @hide */
+    @NonNull
+    public static BluetoothDeviceFilter nullsafe(@Nullable BluetoothDeviceFilter nullable) {
+        return nullable != null ? nullable : noOp();
+    }
+
+    /** @hide */
+    @NonNull
+    public static BluetoothDeviceFilter noOp() {
+        if (NO_OP == null) NO_OP = new Builder().build();
+        return NO_OP;
+    }
+
+    /** @hide */
+    @Override
+    public boolean matches(BluetoothDevice device) {
+        return matchesAddress(mAddress, device)
+                && matchesServiceUuids(mServiceUuids, mServiceUuidMasks, device)
+                && matchesName(getNamePattern(), device);
+    }
+
+    /** @hide */
+    @Nullable
+    public Pattern getNamePattern() {
+        return mNamePattern;
+    }
+
+    /** @hide */
+    @Nullable
+    public String getAddress() {
+        return mAddress;
+    }
+
+    /** @hide */
+    @NonNull
+    public List<ParcelUuid> getServiceUuids() {
+        return mServiceUuids;
+    }
+
+    /** @hide */
+    @NonNull
+    public List<ParcelUuid> getServiceUuidMasks() {
+        return mServiceUuidMasks;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(patternToString(getNamePattern()));
+        dest.writeString(mAddress);
+        dest.writeParcelableList(mServiceUuids, flags);
+        dest.writeParcelableList(mServiceUuidMasks, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<BluetoothDeviceFilter> CREATOR
+            = new Creator<BluetoothDeviceFilter>() {
+        @Override
+        public BluetoothDeviceFilter createFromParcel(Parcel in) {
+            return new BluetoothDeviceFilter(in);
+        }
+
+        @Override
+        public BluetoothDeviceFilter[] newArray(int size) {
+            return new BluetoothDeviceFilter[size];
+        }
+    };
+
+    /**
+     * A builder for {@link BluetoothDeviceFilter}
+     */
+    public static final class Builder extends OneTimeUseBuilder<BluetoothDeviceFilter> {
+        private Pattern mNamePattern;
+        private String mAddress;
+        private ArrayList<ParcelUuid> mServiceUuid;
+        private ArrayList<ParcelUuid> mServiceUuidMask;
+
+        /**
+         * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the
+         *              given regular expression will be shown
+         */
+        public Builder setNamePattern(@Nullable Pattern regex) {
+            checkNotUsed();
+            mNamePattern = regex;
+            return this;
+        }
+
+        /**
+         * @param address if set, only devices with MAC address exactly matching the given one will
+         *                pass the filter
+         */
+        @NonNull
+        public Builder setAddress(@Nullable String address) {
+            checkNotUsed();
+            mAddress = address;
+            return this;
+        }
+
+        /**
+         * Add filtering by certain bits of {@link BluetoothDevice#getUuids()}
+         *
+         * A device with any uuid matching the given bits is considered passing
+         *
+         * @param serviceUuid the values for the bits to match
+         * @param serviceUuidMask if provided, only those bits would have to match.
+         */
+        @NonNull
+        public Builder addServiceUuid(
+                @Nullable ParcelUuid serviceUuid, @Nullable ParcelUuid serviceUuidMask) {
+            checkNotUsed();
+            mServiceUuid = ArrayUtils.add(mServiceUuid, serviceUuid);
+            mServiceUuidMask = ArrayUtils.add(mServiceUuidMask, serviceUuidMask);
+            return this;
+        }
+
+        /** @inheritDoc */
+        @Override
+        @NonNull
+        public BluetoothDeviceFilter build() {
+            markUsed();
+            return new BluetoothDeviceFilter(
+                    mNamePattern, mAddress, mServiceUuid, mServiceUuidMask);
+        }
+    }
+}
diff --git a/core/java/android/companion/BluetoothDeviceFilterUtils.java b/core/java/android/companion/BluetoothDeviceFilterUtils.java
new file mode 100644
index 0000000..289f995
--- /dev/null
+++ b/core/java/android/companion/BluetoothDeviceFilterUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+
+package android.companion;
+
+import static android.text.TextUtils.firstNotEmpty;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/** @hide */
+public class BluetoothDeviceFilterUtils {
+    private BluetoothDeviceFilterUtils() {}
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "BluetoothDeviceFilterUtil";
+
+    @Nullable
+    static String patternToString(@Nullable Pattern p) {
+        return p == null ? null : p.pattern();
+    }
+
+    @Nullable
+    static Pattern patternFromString(@Nullable String s) {
+        return s == null ? null : Pattern.compile(s);
+    }
+
+    static boolean matches(ScanFilter filter, BluetoothDevice device) {
+        return matchesAddress(filter.getDeviceAddress(), device)
+                && matchesServiceUuid(filter.getServiceUuid(), filter.getServiceUuidMask(), device);
+    }
+
+    static boolean matchesAddress(String deviceAddress, BluetoothDevice device) {
+        final boolean result = deviceAddress == null
+                || (device == null || !deviceAddress.equals(device.getAddress()));
+        if (DEBUG) debugLogMatchResult(result, device, deviceAddress);
+        return result;
+    }
+
+    static boolean matchesServiceUuids(List<ParcelUuid> serviceUuids,
+            List<ParcelUuid> serviceUuidMasks, BluetoothDevice device) {
+        for (int i = 0; i < serviceUuids.size(); i++) {
+            ParcelUuid uuid = serviceUuids.get(i);
+            ParcelUuid uuidMask = serviceUuidMasks.get(i);
+            if (!matchesServiceUuid(uuid, uuidMask, device)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean matchesServiceUuid(ParcelUuid serviceUuid, ParcelUuid serviceUuidMask,
+            BluetoothDevice device) {
+        final boolean result = serviceUuid == null ||
+                ScanFilter.matchesServiceUuids(
+                        serviceUuid,
+                        serviceUuidMask,
+                        Arrays.asList(device.getUuids()));
+        if (DEBUG) debugLogMatchResult(result, device, serviceUuid);
+        return result;
+    }
+
+    static boolean matchesName(@Nullable Pattern namePattern, BluetoothDevice device) {
+        boolean result;
+        if (namePattern == null)  {
+            result = true;
+        } else if (device == null) {
+            result = false;
+        } else {
+            final String name = device.getName();
+            result = name != null && namePattern.matcher(name).find();
+        }
+        if (DEBUG) debugLogMatchResult(result, device, namePattern);
+        return result;
+    }
+
+    private static void debugLogMatchResult(
+            boolean result, BluetoothDevice device, Object criteria) {
+        Log.i(LOG_TAG, getDeviceDisplayName(device) + (result ? " ~ " : " !~ ") + criteria);
+    }
+
+    public static String getDeviceDisplayName(@NonNull BluetoothDevice device) {
+        return firstNotEmpty(device.getAliasName(), device.getAddress());
+    }
+}
diff --git a/core/java/android/companion/BluetoothLEDeviceFilter.aidl b/core/java/android/companion/BluetoothLEDeviceFilter.aidl
new file mode 100644
index 0000000..628cf7b
--- /dev/null
+++ b/core/java/android/companion/BluetoothLEDeviceFilter.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+parcelable BluetoothLEDeviceFilter;
diff --git a/core/java/android/companion/BluetoothLEDeviceFilter.java b/core/java/android/companion/BluetoothLEDeviceFilter.java
new file mode 100644
index 0000000..4a481ca
--- /dev/null
+++ b/core/java/android/companion/BluetoothLEDeviceFilter.java
@@ -0,0 +1,151 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+import static android.companion.BluetoothDeviceFilterUtils.patternFromString;
+import static android.companion.BluetoothDeviceFilterUtils.patternToString;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
+import android.provider.OneTimeUseBuilder;
+
+import com.android.internal.util.ObjectUtils;
+
+import java.util.regex.Pattern;
+
+/**
+ * A filter for Bluetooth LE devices
+ *
+ * @see ScanFilter
+ */
+public final class BluetoothLEDeviceFilter implements DeviceFilter<BluetoothDevice> {
+
+    private static BluetoothLEDeviceFilter NO_OP;
+
+    private final Pattern mNamePattern;
+    private final ScanFilter mScanFilter;
+
+    private BluetoothLEDeviceFilter(Pattern namePattern, ScanFilter scanFilter) {
+        mNamePattern = namePattern;
+        mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY);
+    }
+
+    @SuppressLint("ParcelClassLoader")
+    private BluetoothLEDeviceFilter(Parcel in) {
+        this(
+            patternFromString(in.readString()),
+            in.readParcelable(null));
+    }
+
+    /** @hide */
+    @NonNull
+    public static BluetoothLEDeviceFilter nullsafe(@Nullable BluetoothLEDeviceFilter nullable) {
+        return nullable != null ? nullable : noOp();
+    }
+
+    /** @hide */
+    @NonNull
+    public static BluetoothLEDeviceFilter noOp() {
+        if (NO_OP == null) NO_OP = new Builder().build();
+        return NO_OP;
+    }
+
+    /** @hide */
+    @Nullable
+    public Pattern getNamePattern() {
+        return mNamePattern;
+    }
+
+    /** @hide */
+    @NonNull
+    public ScanFilter getScanFilter() {
+        return mScanFilter;
+    }
+
+    /** @hide */
+    @Override
+    public boolean matches(BluetoothDevice device) {
+        return BluetoothDeviceFilterUtils.matches(getScanFilter(), device)
+                && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(patternToString(getNamePattern()));
+        dest.writeParcelable(mScanFilter, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<BluetoothLEDeviceFilter> CREATOR
+            = new Creator<BluetoothLEDeviceFilter>() {
+        @Override
+        public BluetoothLEDeviceFilter createFromParcel(Parcel in) {
+            return new BluetoothLEDeviceFilter(in);
+        }
+
+        @Override
+        public BluetoothLEDeviceFilter[] newArray(int size) {
+            return new BluetoothLEDeviceFilter[size];
+        }
+    };
+
+    /**
+     * Builder for {@link BluetoothLEDeviceFilter}
+     */
+    public static final class Builder extends OneTimeUseBuilder<BluetoothLEDeviceFilter> {
+        private ScanFilter mScanFilter;
+        private Pattern mNamePattern;
+
+        /**
+         * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the
+         *              given regular expression will be shown
+         */
+        public Builder setNamePattern(@Nullable Pattern regex) {
+            checkNotUsed();
+            mNamePattern = regex;
+            return this;
+        }
+
+        /**
+         * @param scanFilter a {@link ScanFilter} to filter devices by
+         *
+         * @see ScanFilter for specific details on its various fields
+         */
+        @NonNull
+        public Builder setScanFilter(@Nullable ScanFilter scanFilter) {
+            checkNotUsed();
+            mScanFilter = scanFilter;
+            return this;
+        }
+
+        /** @inheritDoc */
+        @Override
+        @NonNull
+        public BluetoothLEDeviceFilter build() {
+            markUsed();
+            return new BluetoothLEDeviceFilter(mNamePattern, mScanFilter);
+        }
+    }
+}
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
new file mode 100644
index 0000000..b379c7c
--- /dev/null
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.IntentSender;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+
+/**
+ * System level service for managing companion devices
+ *
+ * Usage:
+ * To obtain an instance call
+ * {@link Context#getSystemService}({@link Context#COMPANION_DEVICE_SERVICE})
+ *
+ * Then, call {@link #associate} to initiate the flow of associating current package
+ * with a device selected by user
+ *
+ * @see AssociationRequest
+ */
+public final class CompanionDeviceManager {
+
+    /**
+     * A device, returned in the activity result of the {@link IntentSender} received in
+     * {@link Callback#onDeviceFound}
+     */
+    public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
+
+    /**
+     * A callback to receive once at least one suitable device is found, or the search failed
+     * (e.g. timed out)
+     */
+    public abstract static class Callback {
+
+        /**
+         * Called once at least one suitable device is found
+         *
+         * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a
+         *                        device
+         */
+        public abstract void onDeviceFound(IntentSender chooserLauncher);
+
+        /**
+         * Called if there was an error looking for device(s), e.g. timeout
+         *
+         * @param error the cause of the error
+         */
+        public abstract void onFailure(CharSequence error);
+    }
+
+    private final ICompanionDeviceManager mService;
+    private final Context mContext;
+
+    /** @hide */
+    public CompanionDeviceManager(
+            @NonNull ICompanionDeviceManager service, @NonNull Context context) {
+        mService = service;
+        mContext = context;
+    }
+
+    /**
+     * Associate this app with a companion device, selected by user
+     *
+     * Once at least one appropriate device is found, {@code callback} will be called with a
+     * {@link PendingIntent} that can be used to show the list of available devices for the user
+     * to select.
+     * It should be started for result (i.e. using
+     * {@link android.app.Activity#startIntentSenderForResult}), as the resulting
+     * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected
+     * device. (e.g. {@link android.bluetooth.BluetoothDevice})
+     *
+     * @param request specific details about this request
+     * @param callback will be called once there's at least one device found for user to choose from
+     * @param handler A handler to control which thread the callback will be delivered on, or null,
+     *                to deliver it on main thread
+     *
+     * @see AssociationRequest
+     */
+    public void associate(
+            @NonNull AssociationRequest<?> request,
+            @NonNull Callback callback,
+            @Nullable Handler handler) {
+        final Handler finalHandler = handler != null
+                ? handler
+                : new Handler(Looper.getMainLooper());
+        try {
+            mService.associate(
+                    request,
+                    new IOnAssociateCallback.Stub() {
+
+                        @Override
+                        public void onSuccess(PendingIntent launcher) throws RemoteException {
+                            finalHandler.post(() -> {
+                                callback.onDeviceFound(launcher.getIntentSender());
+                            });
+                        }
+
+                        @Override
+                        public void onFailure(CharSequence reason) throws RemoteException {
+                            finalHandler.post(() -> callback.onFailure(reason));
+                        }
+                    },
+                    mContext.getPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void requestNotificationAccess() {
+        //TODO implement
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+
+    /** @hide */
+    public boolean haveNotificationAccess() {
+        //TODO implement
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+
+}
diff --git a/core/java/android/companion/DeviceFilter.java b/core/java/android/companion/DeviceFilter.java
new file mode 100644
index 0000000..8362b2d
--- /dev/null
+++ b/core/java/android/companion/DeviceFilter.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+
+package android.companion;
+
+import android.annotation.Nullable;
+import android.os.Parcelable;
+
+/**
+ * A filter for companion devices of type {@code D}
+ *
+ * @param <D> Type of devices, filtered by this filter,
+ *           e.g. {@link android.bluetooth.BluetoothDevice}, {@link android.net.wifi.WifiInfo}
+ */
+public interface DeviceFilter<D extends Parcelable> extends Parcelable {
+
+    /**
+     * @return whether the given device matches this filter
+     *
+     * @hide
+     */
+    boolean matches(D device);
+
+    /**
+     * A nullsafe {@link #matches(Parcelable)}, returning true if the filter is null
+     *
+     * @hide
+     */
+    static <D extends Parcelable> boolean matches(@Nullable DeviceFilter<D> filter, D device) {
+        return filter == null || filter.matches(device);
+    }
+}
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
new file mode 100644
index 0000000..065e31b
--- /dev/null
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+import android.companion.IOnAssociateCallback;
+import android.companion.AssociationRequest;
+
+/**
+ * Interface for communication with the core companion device manager service.
+ *
+ * @hide
+ */
+interface ICompanionDeviceManager {
+    void associate(in AssociationRequest request,
+        in IOnAssociateCallback callback,
+        in String callingPackage); //TODO int userId?
+
+    //TODO add these
+//    boolean haveNotificationAccess(String packageName);
+//    oneway void requestNotificationAccess(String packageName);
+}
diff --git a/core/java/android/companion/ICompanionDeviceManagerService.aidl b/core/java/android/companion/ICompanionDeviceManagerService.aidl
new file mode 100644
index 0000000..ff2a7eb
--- /dev/null
+++ b/core/java/android/companion/ICompanionDeviceManagerService.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+package android.companion;
+
+import android.companion.AssociationRequest;
+import android.companion.IOnAssociateCallback;
+
+
+/** @hide */
+interface ICompanionDeviceManagerService {
+    void startDiscovery(
+        in AssociationRequest request,
+        in IOnAssociateCallback callback,
+        in String callingPackage);
+}
diff --git a/core/java/android/companion/ICompanionDeviceManagerServiceCallback.aidl b/core/java/android/companion/ICompanionDeviceManagerServiceCallback.aidl
new file mode 100644
index 0000000..c9dd345
--- /dev/null
+++ b/core/java/android/companion/ICompanionDeviceManagerServiceCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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 per  missions and
+ * limitations under the License.
+ */
+
+package android.companion;
+
+import android.bluetooth.BluetoothDevice;
+
+/** @hide */
+interface ICompanionDeviceManagerServiceCallback {
+    void onDeviceSelected(in BluetoothDevice device);
+}
diff --git a/core/java/android/companion/IOnAssociateCallback.aidl b/core/java/android/companion/IOnAssociateCallback.aidl
new file mode 100644
index 0000000..4867eadd
--- /dev/null
+++ b/core/java/android/companion/IOnAssociateCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 per  missions and
+ * limitations under the License.
+ */
+
+package android.companion;
+
+import android.app.PendingIntent;
+
+/** @hide */
+interface IOnAssociateCallback {
+    void onSuccess(in PendingIntent launcher);
+    void onFailure(in CharSequence reason);
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index a7d5ac6..06f7303 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3600,6 +3600,14 @@
     public static final String PRINT_SERVICE = "print";
 
     /**
+     * {@link android.companion.CompanionDeviceManager} for managing companion devices
+     *
+     * @see #getSystemService
+     * @see android.companion.CompanionDeviceManager
+     */
+    public static final String COMPANION_DEVICE_SERVICE = "companion_device";
+
+    /**
      * Use with {@link #getSystemService} to retrieve a
      * {@link android.hardware.ConsumerIrManager} for transmitting infrared
      * signals from the device.
diff --git a/core/java/android/provider/OneTimeUseBuilder.java b/core/java/android/provider/OneTimeUseBuilder.java
new file mode 100644
index 0000000..682e9c7
--- /dev/null
+++ b/core/java/android/provider/OneTimeUseBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+
+package android.provider;
+
+/**
+ * A builder that facilitates prohibiting its use after an instance was created with it.
+ *
+ * Suggested usage:
+ * call {@link #checkNotUsed} in each setter, and {@link #markUsed} in {@link #build}
+ *
+ * @param <T> Type of object being built
+ * @hide
+ */
+public abstract class OneTimeUseBuilder<T> {
+    private boolean used = false;
+
+    protected void markUsed() {
+        checkNotUsed();
+        used = true;
+    }
+
+    protected void checkNotUsed() {
+        if (used) {
+            throw new IllegalStateException(
+                    "This Builder should not be reused. Use a new Builder instance instead");
+        }
+    }
+
+    /**
+     * Builds the instance
+     *
+     * Once this method is called, this builder should no longer be used. Any subsequent calls to a
+     * setter or {@code build()} will throw an exception
+     */
+    public abstract T build();
+}
diff --git a/core/java/android/provider/SettingsStringUtil.java b/core/java/android/provider/SettingsStringUtil.java
new file mode 100644
index 0000000..f242d79
--- /dev/null
+++ b/core/java/android/provider/SettingsStringUtil.java
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+
+package android.provider;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.text.TextUtils;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.function.Function;
+
+/**
+ * Utilities for dealing with {@link String} values in {@link Settings}
+ *
+ * @hide
+ */
+public class SettingsStringUtil {
+    private SettingsStringUtil() {}
+
+    public static final String DELIMITER = ":";
+
+    /**
+     * A {@link HashSet} of items, that uses a common convention of setting string
+     * serialization/deserialization of separating multiple items with {@link #DELIMITER}
+     */
+    public static abstract class ColonDelimitedSet<T> extends HashSet<T> {
+
+        public ColonDelimitedSet(String colonSeparatedItems) {
+            for (String cn :
+                    TextUtils.split(TextUtils.emptyIfNull(colonSeparatedItems), DELIMITER)) {
+                add(itemFromString(cn));
+            }
+        }
+
+        protected abstract T itemFromString(String s);
+        protected String itemToString(T item) {
+            return String.valueOf(item);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            Iterator<T> it = iterator();
+            if (it.hasNext()) {
+                sb.append(it.next());
+                while (it.hasNext()) {
+                    sb.append(DELIMITER);
+                    sb.append(itemToString(it.next()));
+                }
+            }
+            return sb.toString();
+        }
+
+
+        public static class OfStrings extends ColonDelimitedSet<String> {
+            public OfStrings(String colonSeparatedItems) {
+                super(colonSeparatedItems);
+            }
+
+            @Override
+            protected String itemFromString(String s) {
+                return s;
+            }
+
+            public static String add(String delimitedElements, String element) {
+                final ColonDelimitedSet<String> set
+                        = new ColonDelimitedSet.OfStrings(delimitedElements);
+                if (set.contains(element)) {
+                    return delimitedElements;
+                }
+                set.add(element);
+                return set.toString();
+            }
+
+            public static String remove(String delimitedElements, String element) {
+                final ColonDelimitedSet<String> set
+                        = new ColonDelimitedSet.OfStrings(delimitedElements);
+                if (!set.contains(element)) {
+                    return delimitedElements;
+                }
+                set.remove(element);
+                return set.toString();
+            }
+
+            public static boolean contains(String delimitedElements, String element) {
+                final String[] elements = TextUtils.split(delimitedElements, DELIMITER);
+                return ArrayUtils.indexOf(elements, element) != -1;
+            }
+        }
+    }
+
+    public static class ComponentNameSet extends ColonDelimitedSet<ComponentName> {
+        public ComponentNameSet(String colonSeparatedPackageNames) {
+            super(colonSeparatedPackageNames);
+        }
+
+        @Override
+        protected ComponentName itemFromString(String s) {
+            return ComponentName.unflattenFromString(s);
+        }
+
+        @Override
+        protected String itemToString(ComponentName item) {
+            return item.flattenToString();
+        }
+
+        public static String add(String delimitedElements, ComponentName element) {
+            final ComponentNameSet set = new ComponentNameSet(delimitedElements);
+            if (set.contains(element)) {
+                return delimitedElements;
+            }
+            set.add(element);
+            return set.toString();
+        }
+
+        public static String remove(String delimitedElements, ComponentName element) {
+            final ComponentNameSet set = new ComponentNameSet(delimitedElements);
+            if (!set.contains(element)) {
+                return delimitedElements;
+            }
+            set.remove(element);
+            return set.toString();
+        }
+
+        public static boolean contains(String delimitedElements, ComponentName element) {
+            return ColonDelimitedSet.OfStrings.contains(
+                    delimitedElements, element.flattenToString());
+        }
+    }
+
+    public static class SettingStringHelper {
+        private final ContentResolver mContentResolver;
+        private final String mSettingName;
+        private final int mUserId;
+
+        public SettingStringHelper(ContentResolver contentResolver, String name, int userId) {
+            mContentResolver = contentResolver;
+            mUserId = userId;
+            mSettingName = name;
+        }
+
+        public String read() {
+            return Settings.Secure.getStringForUser(
+                    mContentResolver, mSettingName, mUserId);
+        }
+
+        public boolean write(String value) {
+            return Settings.Secure.putStringForUser(
+                    mContentResolver, mSettingName, value, mUserId);
+        }
+
+        public boolean modify(Function<String, String> change) {
+            return write(change.apply(read()));
+        }
+    }
+}
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index bb952ab..55aeb1e 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -62,6 +62,7 @@
 
 import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
 
 import java.lang.reflect.Array;
 import java.util.Iterator;
@@ -466,6 +467,16 @@
         return isEmpty(str) ? null : str;
     }
 
+    /** {@hide} */
+    public static String emptyIfNull(@Nullable String str) {
+        return str == null ? "" : str;
+    }
+
+    /** {@hide} */
+    public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
+        return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
+    }
+
     /**
      * Returns the length that the specified CharSequence would have if
      * spaces and ASCII control characters were trimmed from the start and end,
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index a9a6364..cb3a250 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -435,10 +435,6 @@
         }
     }
 
-    public static <T> boolean contains(@Nullable ArraySet<T> cur, T val) {
-        return (cur != null) ? cur.contains(val) : false;
-    }
-
     public static @NonNull <T> ArrayList<T> add(@Nullable ArrayList<T> cur, T val) {
         if (cur == null) {
             cur = new ArrayList<>();
@@ -459,6 +455,16 @@
         }
     }
 
+    /**
+     * Returns the given list, or an immutable empty list if the provided list is null
+     *
+     * @see Collections#emptyList
+     */
+
+    public static @NonNull <T> List<T> emptyIfNull(@Nullable List<T> cur) {
+        return cur == null ? Collections.emptyList() : cur;
+    }
+
     public static <T> boolean contains(@Nullable Collection<T> cur, T val) {
         return (cur != null) ? cur.contains(val) : false;
     }
diff --git a/core/java/com/android/internal/util/ObjectUtils.java b/core/java/com/android/internal/util/ObjectUtils.java
new file mode 100644
index 0000000..d109a5a
--- /dev/null
+++ b/core/java/com/android/internal/util/ObjectUtils.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** @hide */
+public class ObjectUtils {
+    private ObjectUtils() {}
+
+    @NonNull
+    public static <T> T firstNotNull(@Nullable T a, @NonNull T b) {
+        return a != null ? a : Preconditions.checkNotNull(b);
+    }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 11eb47b..a6e43ff 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2365,6 +2365,11 @@
     <permission android:name="android.permission.BIND_PRINT_SPOOLER_SERVICE"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by the CompanionDeviceManager to ensure that only the system can bind to it.
+         @hide -->
+    <permission android:name="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
+        android:protectionLevel="signature" />
+
     <!-- @SystemApi Must be required by the RuntimePermissionPresenterService to ensure
          that only the system can bind to it.
          @hide -->
diff --git a/packages/CompanionDeviceManager/Android.mk b/packages/CompanionDeviceManager/Android.mk
new file mode 100644
index 0000000..f730356
--- /dev/null
+++ b/packages/CompanionDeviceManager/Android.mk
@@ -0,0 +1,27 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CompanionDeviceManager
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml
new file mode 100644
index 0000000..3eede54
--- /dev/null
+++ b/packages/CompanionDeviceManager/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2017 Google Inc.
+ *
+ * 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.
+ */
+-->
+<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.ACCESS_COARSE_LOCATION"/>
+
+    <application
+        android:allowClearUserData="true"
+        android:label="@string/app_label"
+        android:allowBackup="false"
+        android:supportsRtl="true">
+
+        <service
+            android:name=".DeviceDiscoveryService"
+            android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
+            android:exported="true">
+        </service>
+
+        <activity
+            android:name=".DeviceChooserActivity"
+            android:theme="@*android:style/Theme.Dialog.NoFrame"
+            android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE">
+            <!--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>
+
+    </application>
+
+</manifest>
diff --git a/packages/CompanionDeviceManager/MODULE_LICENSE_APACHE2 b/packages/CompanionDeviceManager/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/CompanionDeviceManager/MODULE_LICENSE_APACHE2
diff --git a/packages/CompanionDeviceManager/NOTICE b/packages/CompanionDeviceManager/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/packages/CompanionDeviceManager/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/packages/CompanionDeviceManager/res/layout/device_chooser.xml b/packages/CompanionDeviceManager/res/layout/device_chooser.xml
new file mode 100644
index 0000000..ee08582
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/layout/device_chooser.xml
@@ -0,0 +1,67 @@
+<?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:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_weight="0.1"
+    >
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textColor="@android:color/black"
+        style="@*android:style/TextAppearance.Widget.Toolbar.Title"
+    />
+
+    <ListView
+        android:id="@+id/device_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/title"
+        android:layout_above="@+id/buttons"
+        style="@android:style/Widget.Material.Light.ListView"
+    />
+
+    <LinearLayout
+        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="Cancel"
+            style="@android:style/Widget.Material.Light.Button.Borderless.Colored"
+        />
+        <Button
+            android:id="@+id/button_pair"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Pair"
+            style="@android:style/Widget.Material.Light.Button.Borderless.Colored"
+        />
+    </LinearLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/dimens.xml b/packages/CompanionDeviceManager/res/values/dimens.xml
new file mode 100644
index 0000000..da7b0d1
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <!--  Padding applied on most UI elements  -->
+    <dimen name="padding">12dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
new file mode 100644
index 0000000..c4195b50
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- 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">Pair with &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; via Bluetooth?</string>
+
+</resources>
diff --git a/packages/CompanionDeviceManager/res/values/themes.xml b/packages/CompanionDeviceManager/res/values/themes.xml
new file mode 100644
index 0000000..465f8fc
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?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>
+
+    <!--TODO-->
+    <!--<style name="Theme.ChooserActivity" parent="@*android:style/Theme.Dialog.NoFrame">-->
+        <!--&lt;!&ndash;<item name="android:windowBackground">@android:color/light_grey</item>&ndash;&gt;-->
+        <!--<item name="android:backgroundColor">@android:color/light_grey</item>-->
+    <!--</style>-->
+
+</resources>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java
new file mode 100644
index 0000000..c95f940
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+
+package com.android.companiondevicemanager;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothDevice;
+import android.companion.CompanionDeviceManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.Html;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class DeviceChooserActivity extends Activity {
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "DeviceChooserActivity";
+
+    private ListView mDeviceListView;
+    private View mPairButton;
+    private View mCancelButton;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (DEBUG) Log.i(LOG_TAG, "Started with intent " + getIntent());
+
+        setContentView(R.layout.device_chooser);
+        setTitle(Html.fromHtml(getString(R.string.chooser_title, getCallingAppName()), 0));
+        getWindow().getDecorView().getRootView().setBackgroundColor(Color.LTGRAY); //TODO theme
+
+        if (getService().mDevicesFound.isEmpty()) {
+            Log.e(LOG_TAG, "About to show UI, but no devices to show");
+        }
+
+        final DeviceDiscoveryService.DevicesAdapter adapter = getService().mDevicesAdapter;
+        mDeviceListView = (ListView) findViewById(R.id.device_list);
+        mDeviceListView.setAdapter(adapter);
+        mDeviceListView.addFooterView(getProgressBar(), null, false);
+
+        mPairButton = findViewById(R.id.button_pair);
+        mPairButton.setOnClickListener((view) ->
+                onPairTapped(getService().mSelectedDevice));
+        adapter.registerDataSetObserver(new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                updatePairButtonEnabled();
+            }
+        });
+        updatePairButtonEnabled();
+        mCancelButton = findViewById(R.id.button_cancel);
+        mCancelButton.setOnClickListener((view) -> {
+            setResult(RESULT_CANCELED);
+            finish();
+        });
+    }
+
+    private CharSequence getCallingAppName() {
+        try {
+            final PackageManager packageManager = getPackageManager();
+            return packageManager.getApplicationLabel(
+                    packageManager.getApplicationInfo(getService().mCallingPackage, 0));
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        final TextView titleView = (TextView) findViewById(R.id.title);
+        final int padding = getPadding(getResources());
+        titleView.setPadding(padding, padding, padding, padding);
+        titleView.setText(title);
+    }
+
+    //TODO put in resources xmls
+    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;
+    }
+
+    static int getPadding(Resources r) {
+        return r.getDimensionPixelSize(R.dimen.padding);
+        //TODO
+//        final float dp = r.getDisplayMetrics().density;
+//        return (int)(12 * dp);
+    }
+
+    private void updatePairButtonEnabled() {
+        mPairButton.setEnabled(getService().mSelectedDevice != null);
+    }
+
+    private DeviceDiscoveryService getService() {
+        return DeviceDiscoveryService.sInstance;
+    }
+
+    protected void onPairTapped(BluetoothDevice selectedDevice) {
+        setResult(RESULT_OK,
+                new Intent().putExtra(CompanionDeviceManager.EXTRA_DEVICE, selectedDevice));
+        finish();
+    }
+
+    private void toast(String msg) {
+        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+    }
+}
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java
new file mode 100644
index 0000000..a3eec0d
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2013 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.getDeviceDisplayName;
+import static android.companion.BluetoothLEDeviceFilter.nullsafe;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.companion.AssociationRequest;
+import android.companion.BluetoothDeviceFilterUtils;
+import android.companion.BluetoothLEDeviceFilter;
+import android.companion.ICompanionDeviceManagerService;
+import android.companion.IOnAssociateCallback;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class DeviceDiscoveryService extends Service {
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "DeviceDiscoveryService";
+
+    static DeviceDiscoveryService sInstance;
+
+    private BluetoothAdapter mBluetoothAdapter;
+    private BluetoothLEDeviceFilter mFilter;
+    private ScanFilter mScanFilter;
+    private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build();
+    List<BluetoothDevice> mDevicesFound;
+    BluetoothDevice mSelectedDevice;
+    DevicesAdapter mDevicesAdapter;
+    IOnAssociateCallback mCallback;
+    String mCallingPackage;
+
+    private final ICompanionDeviceManagerService mBinder =
+            new ICompanionDeviceManagerService.Stub() {
+        @Override
+        public void startDiscovery(AssociationRequest request,
+                IOnAssociateCallback callback,
+                String callingPackage) throws RemoteException {
+            if (DEBUG) {
+                Log.i(LOG_TAG,
+                        "startDiscovery() called with: filter = [" + request + "], callback = ["
+                                + callback + "]");
+            }
+            mCallback = callback;
+            mCallingPackage = callingPackage;
+            DeviceDiscoveryService.this.startDiscovery(request);
+        }
+    };
+
+    private final ScanCallback mBLEScanCallback = new ScanCallback() {
+        @Override
+        public void onScanResult(int callbackType, ScanResult result) {
+            final BluetoothDevice device = result.getDevice();
+            if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
+                onDeviceLost(device);
+            } else {
+                onDeviceFound(device);
+            }
+        }
+    };
+
+    private BluetoothLeScanner mBLEScanner;
+
+    private BroadcastReceiver mBluetoothDeviceFoundBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final BluetoothDevice device = intent.getParcelableExtra(
+                    BluetoothDevice.EXTRA_DEVICE);
+            if (!mFilter.matches(device)) return; // ignore device
+
+            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
+                onDeviceFound(device);
+            } else {
+                onDeviceLost(device);
+            }
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")");
+        return mBinder.asBinder();
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        if (DEBUG) Log.i(LOG_TAG, "onCreate()");
+
+        mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
+        mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
+
+        mDevicesFound = new ArrayList<>();
+        mDevicesAdapter = new DevicesAdapter();
+
+        sInstance = this;
+    }
+
+    private void startDiscovery(AssociationRequest<?> request) {
+        //TODO support other protocols as well
+        mFilter = nullsafe((BluetoothLEDeviceFilter) request.getDeviceFilter());
+        mScanFilter = mFilter.getScanFilter();
+
+        reset();
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
+        intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED);
+
+        registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter);
+        mBluetoothAdapter.startDiscovery();
+
+        mBLEScanner.startScan(
+                Collections.singletonList(mScanFilter), mDefaultScanSettings, mBLEScanCallback);
+    }
+
+    private void reset() {
+        mDevicesFound.clear();
+        mSelectedDevice = null;
+        mDevicesAdapter.notifyDataSetChanged();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        stopScan();
+        return super.onUnbind(intent);
+    }
+
+    private void stopScan() {
+        if (DEBUG) Log.i(LOG_TAG, "stopScan() called");
+        mBluetoothAdapter.cancelDiscovery();
+        mBLEScanner.stopScan(mBLEScanCallback);
+        unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver);
+        stopSelf();
+    }
+
+    private void onDeviceFound(BluetoothDevice device) {
+        if (mDevicesFound.contains(device)) {
+            return;
+        }
+
+        if (DEBUG) {
+            Log.i(LOG_TAG, "Considering device " + getDeviceDisplayName(device));
+        }
+
+        if (!mFilter.matches(device)) {
+            return;
+        }
+
+        if (DEBUG) {
+            Log.i(LOG_TAG, "Found device " + getDeviceDisplayName(device));
+        }
+        if (mDevicesFound.isEmpty()) {
+            onReadyToShowUI();
+        }
+        mDevicesFound.add(device);
+        mDevicesAdapter.notifyDataSetChanged();
+    }
+
+    //TODO also, on timeout -> call onFailure
+    private void onReadyToShowUI() {
+        try {
+            mCallback.onSuccess(PendingIntent.getActivity(
+                    this, 0,
+                    new Intent(this, DeviceChooserActivity.class),
+                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
+                            | PendingIntent.FLAG_IMMUTABLE));
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void onDeviceLost(BluetoothDevice device) {
+        mDevicesFound.remove(device);
+        mDevicesAdapter.notifyDataSetChanged();
+        if (DEBUG) {
+            Log.i(LOG_TAG, "Lost device " + getDeviceDisplayName(device));
+        }
+    }
+
+    class DevicesAdapter extends ArrayAdapter<BluetoothDevice> {
+        private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth);
+
+        private Drawable icon(int drawableRes) {
+            Drawable icon = getResources().getDrawable(drawableRes, null);
+            icon.setTint(Color.DKGRAY);
+            return icon;
+        }
+
+        public DevicesAdapter() {
+            super(DeviceDiscoveryService.this, 0, mDevicesFound);
+        }
+
+        @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, BluetoothDevice device) {
+            textView.setText(getDeviceDisplayName(device));
+            textView.setBackgroundColor(
+                    device.equals(mSelectedDevice)
+                            ? Color.GRAY
+                            : Color.TRANSPARENT);
+            textView.setOnClickListener((view) -> {
+                mSelectedDevice = device;
+                notifyDataSetChanged();
+            });
+        }
+
+        //TODO move to a layout file
+        private TextView newView() {
+            final TextView textView = new TextView(DeviceDiscoveryService.this);
+            textView.setTextColor(Color.BLACK);
+            final int padding = DeviceChooserActivity.getPadding(getResources());
+            textView.setPadding(padding, padding, padding, padding);
+            textView.setCompoundDrawablesWithIntrinsicBounds(
+                    BLUETOOTH_ICON, null, null, null);
+            textView.setCompoundDrawablePadding(padding);
+            return textView;
+        }
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index a861522..c0a2e71 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -70,6 +70,8 @@
 import android.os.UserManagerInternal;
 import android.provider.Settings;
 import android.hardware.fingerprint.IFingerprintService;
+import android.provider.SettingsStringUtil.ComponentNameSet;
+import android.provider.SettingsStringUtil.SettingStringHelper;
 import android.text.TextUtils;
 import android.text.TextUtils.SimpleStringSplitter;
 import android.util.Slog;
@@ -98,9 +100,9 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.os.SomeArgs;
 import com.android.server.LocalServices;
-
 import com.android.server.policy.AccessibilityShortcutController;
 import com.android.server.statusbar.StatusBarManagerInternal;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.FileDescriptor;
@@ -2013,10 +2015,12 @@
      * Enables accessibility service specified by {@param componentName} for the {@param userId}.
      */
     private void enableAccessibilityServiceLocked(ComponentName componentName, int userId) {
-        SettingsStringHelper settingsHelper = new SettingsStringHelper(
-                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
-        settingsHelper.addService(componentName);
-        settingsHelper.writeToSettings();
+        final SettingStringHelper setting =
+                new SettingStringHelper(
+                        mContext.getContentResolver(),
+                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                        userId);
+        setting.write(ComponentNameSet.add(setting.read(), componentName));
 
         UserState userState = getUserStateLocked(userId);
         if (userState.mEnabledServices.add(componentName)) {
@@ -2028,10 +2032,12 @@
      * Disables accessibility service specified by {@param componentName} for the {@param userId}.
      */
     private void disableAccessibilityServiceLocked(ComponentName componentName, int userId) {
-        SettingsStringHelper settingsHelper = new SettingsStringHelper(
-                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
-        settingsHelper.deleteService(componentName);
-        settingsHelper.writeToSettings();
+        final SettingStringHelper setting =
+                new SettingStringHelper(
+                        mContext.getContentResolver(),
+                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                        userId);
+        setting.write(ComponentNameSet.remove(setting.read(), componentName));
 
         UserState userState = getUserStateLocked(userId);
         if (userState.mEnabledServices.remove(componentName)) {
@@ -2060,45 +2066,6 @@
         return mFingerprintGestureDispatcher.onFingerprintGesture(gestureKeyCode);
     }
 
-    private class SettingsStringHelper {
-        private static final String SETTINGS_DELIMITER = ":";
-        private ContentResolver mContentResolver;
-        private final String mSettingsName;
-        private Set<String> mServices;
-        private final int mUserId;
-
-        public SettingsStringHelper(String name, int userId) {
-            mUserId = userId;
-            mSettingsName = name;
-            mContentResolver = mContext.getContentResolver();
-            String servicesString = Settings.Secure.getStringForUser(
-                    mContentResolver, mSettingsName, userId);
-            mServices = new HashSet();
-            if (!TextUtils.isEmpty(servicesString)) {
-                final TextUtils.SimpleStringSplitter colonSplitter =
-                        new TextUtils.SimpleStringSplitter(SETTINGS_DELIMITER.charAt(0));
-                colonSplitter.setString(servicesString);
-                while (colonSplitter.hasNext()) {
-                    final String serviceName = colonSplitter.next();
-                    mServices.add(serviceName);
-                }
-            }
-        }
-
-        public void addService(ComponentName component) {
-            mServices.add(component.flattenToString());
-        }
-
-        public void deleteService(ComponentName component) {
-            mServices.remove(component.flattenToString());
-        }
-
-        public void writeToSettings() {
-            Settings.Secure.putStringForUser(mContentResolver, mSettingsName,
-                    TextUtils.join(SETTINGS_DELIMITER, mServices), mUserId);
-        }
-    }
-
     @Override
     public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
         mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3eebd89..83e209d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -154,6 +154,8 @@
             "com.android.server.voiceinteraction.VoiceInteractionManagerService";
     private static final String PRINT_MANAGER_SERVICE_CLASS =
             "com.android.server.print.PrintManagerService";
+    private static final String COMPANION_DEVICE_MANAGER_SERVICE_CLASS =
+            "com.android.server.print.CompanionDeviceManagerService";
     private static final String USB_SERVICE_CLASS =
             "com.android.server.usb.UsbService$Lifecycle";
     private static final String MIDI_SERVICE_CLASS =
@@ -1350,6 +1352,10 @@
                 traceEnd();
             }
 
+            traceBeginAndSlog("StartCompanionDeviceManager");
+            mSystemServiceManager.startService(COMPANION_DEVICE_MANAGER_SERVICE_CLASS);
+            traceEnd();
+
             traceBeginAndSlog("StartRestrictionManager");
             mSystemServiceManager.startService(RestrictionsManagerService.class);
             traceEnd();
diff --git a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java
new file mode 100644
index 0000000..9824c1d
--- /dev/null
+++ b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+
+package com.android.server.print;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.app.PendingIntent;
+import android.companion.AssociationRequest;
+import android.companion.ICompanionDeviceManager;
+import android.companion.ICompanionDeviceManagerService;
+import android.companion.IOnAssociateCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+//TODO move to own package!
+/** @hide */
+public class CompanionDeviceManagerService extends SystemService {
+
+    private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
+            "com.android.companiondevicemanager", ".DeviceDiscoveryService");
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "CompanionDeviceManagerService";
+
+    private final CompanionDeviceManagerImpl mImpl;
+
+    public CompanionDeviceManagerService(Context context) {
+        super(context);
+        mImpl = new CompanionDeviceManagerImpl();
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
+    }
+
+    class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
+
+        @Override
+        public void associate(
+                AssociationRequest request,
+                IOnAssociateCallback callback,
+                String callingPackage) throws RemoteException {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
+                        + ", callingPackage = " + callingPackage + ")");
+            }
+            checkNotNull(request);
+            checkNotNull(callback);
+            final long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                //TODO bindServiceAsUser
+                getContext().bindService(
+                        new Intent().setComponent(SERVICE_TO_BIND_TO),
+                        getServiceConnection(request, callback, callingPackage),
+                        Context.BIND_AUTO_CREATE);
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+    }
+
+    private ServiceConnection getServiceConnection(
+            final AssociationRequest<?> request,
+            final IOnAssociateCallback callback,
+            final String callingPackage) {
+        return new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG,
+                            "onServiceConnected(name = " + name + ", service = "
+                                    + service + ")");
+                }
+                try {
+                    ICompanionDeviceManagerService.Stub
+                            .asInterface(service)
+                            .startDiscovery(
+                                    request,
+                                    getCallback(callingPackage, callback),
+                                    callingPackage);
+                } catch (RemoteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                if (DEBUG) Log.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
+            }
+        };
+    }
+
+    private IOnAssociateCallback.Stub getCallback(
+            String callingPackage,
+            IOnAssociateCallback propagateTo) {
+        return new IOnAssociateCallback.Stub() {
+
+            @Override
+            public void onSuccess(PendingIntent launcher)
+                    throws RemoteException {
+                if (DEBUG) Log.i(LOG_TAG, "onSuccess(launcher = " + launcher + ")");
+                recordSpecialPriviledgesForPackage(callingPackage);
+                propagateTo.onSuccess(launcher);
+            }
+
+            @Override
+            public void onFailure(CharSequence reason) throws RemoteException {
+                if (DEBUG) Log.i(LOG_TAG, "onFailure()");
+                propagateTo.onFailure(reason);
+            }
+        };
+    }
+
+    void recordSpecialPriviledgesForPackage(String priviledgedPackage) {
+        //TODO Show dialog before recording notification access
+//        final SettingStringHelper setting =
+//                new SettingStringHelper(
+//                        getContext().getContentResolver(),
+//                        Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+//                        Binder.getCallingUid());
+//        setting.write(ColonDelimitedSet.OfStrings.add(setting.read(), priviledgedPackage));
+    }
+}