diff options
author | Eugene Susla <eugenesusla@google.com> | 2017-01-22 13:52:51 -0800 |
---|---|---|
committer | Eugene Susla <eugenesusla@google.com> | 2017-02-06 12:50:55 -0800 |
commit | 6ed45d8cd33c297e608aba94fc1f61dace7a7cca (patch) | |
tree | 7d62835183a39a490ad2b518b27cdf6c1c232cdf /packages/CompanionDeviceManager/src | |
parent | 556897f7018e1b231d6200e21dd45657f180a63c (diff) |
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
Diffstat (limited to 'packages/CompanionDeviceManager/src')
2 files changed, 406 insertions, 0 deletions
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 000000000000..c95f940807a9 --- /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 000000000000..a3eec0dd5cb5 --- /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; + } + } +} |