Initial (ugly) support for device discovery and pairing (#3)

master
cpfeiffer 2015-05-05 00:48:02 +02:00
parent e859ece7c6
commit 9df661bd96
30 changed files with 930 additions and 49 deletions

View File

@ -5,5 +5,5 @@ host = https://www.transifex.com
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en_US
type = ANDROID
deviceType = ANDROID

View File

@ -1,8 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nodomain.freeyourgadget.gadgetbridge">
package="nodomain.freeyourgadget.gadgetbridge" >
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@ -10,45 +15,50 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.fsck.k9.permission.READ_MESSAGES" />
<uses-sdk android:targetSdkVersion="21" android:minSdkVersion="19"/>
<android:uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
<application
android:name=".GBApplication"
android:allowBackup="true"
android:name="nodomain.freeyourgadget.gadgetbridge.GBApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/GadgetbridgeTheme">
android:theme="@style/GadgetbridgeTheme" >
<activity
android:name=".ControlCenter"
android:label="@string/title_activity_controlcenter">
android:label="@string/title_activity_controlcenter" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings">
android:label="@string/title_activity_settings" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ControlCenter" />
</activity>
<activity
android:name=".AppManagerActivity"
android:label="@string/title_activity_appmanager">
android:label="@string/title_activity_appmanager" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ControlCenter" />
</activity>
<activity
android:name=".pebble.PebbleAppInstallerActivity"
android:label="@string/title_activity_appinstaller">
android:label="@string/title_activity_appinstaller" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".AppManagerActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:host="*" />
@ -61,16 +71,16 @@
<service
android:name=".externalevents.NotificationListener"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service android:name=".BluetoothCommunicationService"></service>
<service android:name=".BluetoothCommunicationService" />
<receiver
android:name=".externalevents.PhoneCallReceiver"
android:enabled="false">
android:enabled="false" >
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
@ -80,16 +90,17 @@
</receiver>
<receiver
android:name=".externalevents.SMSReceiver"
android:enabled="false">
android:enabled="false" >
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.K9Receiver"
android:enabled="false">
android:enabled="false" >
<intent-filter>
<data android:scheme="email" />
<action android:name="com.fsck.k9.intent.action.EMAIL_RECEIVED" />
</intent-filter>
</receiver>
@ -102,28 +113,28 @@
</receiver>
<receiver
android:name=".externalevents.MusicPlaybackReceiver"
android:enabled="false">
android:enabled="false" >
<intent-filter>
<action android:name="com.andrew.apollo.metachanged" />
</intent-filter>
</receiver>
<receiver
android:name=".BluetoothStateChangeReceiver"
android:exported="false">
android:exported="false" >
<intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".GBMusicControlReceiver"
android:exported="false">
android:exported="false" >
<intent-filter>
<action android:name="nodomain.freeyourgadget.gadgetbridge.musiccontrol" />
</intent-filter>
</receiver>
<receiver
android:name=".GBCallControlReceiver"
android:exported="false">
android:exported="false" >
<intent-filter>
<action android:name="nodomain.freeyourgadget.gadgetbridge.callcontrol" />
</intent-filter>
@ -131,11 +142,26 @@
<activity
android:name=".DebugActivity"
android:label="@string/title_activity_debug">
android:label="@string/title_activity_debug" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ControlCenter" />
</activity>
<activity
android:name=".discovery.DiscoveryActivity"
android:label="@string/title_activity_discovery" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ControlCenter" />
</activity>
<activity
android:name=".activities.AndroidPairingActivity"
android:label="@string/title_activity_android_pairing" >
</activity>
<activity
android:name=".miband.MiBandPairingActivity"
android:label="@string/title_activity_mi_band_pairing" >
</activity>
</application>
</manifest>

View File

@ -25,6 +25,12 @@ public abstract class AbstractBTDeviceSupport extends AbstractDeviceSupport {
}
}
@Override
public void pair() {
// Default implementation does no manual pairing, use the Android
// pairing dialog instead.
}
public synchronized GBDeviceProtocol getDeviceProtocol() {
if (gbDeviceProtocol == null) {
gbDeviceProtocol = createDeviceProtocol();

View File

@ -27,6 +27,8 @@ import nodomain.freeyourgadget.gadgetbridge.pebble.PebbleSupport;
public class BluetoothCommunicationService extends Service {
public static final String ACTION_START
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.start";
public static final String ACTION_PAIR
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.pair";
public static final String ACTION_CONNECT
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.connect";
public static final String ACTION_NOTIFICATION_GENERIC
@ -49,8 +51,10 @@ public class BluetoothCommunicationService extends Service {
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.deleteapp";
public static final String ACTION_INSTALL_PEBBLEAPP
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.install_pebbbleapp";
public static final String EXTRA_PERFORM_PAIR = "perform_pair";
private static final String TAG = "CommunicationService";
public static final String EXTRA_DEVICE_ADDRESS = "device_address";
private BluetoothAdapter mBtAdapter = null;
private GBDeviceIoThread mGBDeviceIoThread = null;
@ -90,6 +94,7 @@ public class BluetoothCommunicationService extends Service {
}
String action = intent.getAction();
boolean pair = intent.getBooleanExtra(EXTRA_PERFORM_PAIR, false);
if (action == null) {
Log.i(TAG, "no action");
@ -128,7 +133,7 @@ public class BluetoothCommunicationService extends Service {
} else if (!mBtAdapter.isEnabled()) {
Toast.makeText(this, R.string.bluetooth_is_disabled_, Toast.LENGTH_SHORT).show();
} else {
String btDeviceAddress = intent.getStringExtra("device_address");
String btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
sharedPrefs.edit().putString("last_device_address", btDeviceAddress).apply();
@ -140,15 +145,19 @@ public class BluetoothCommunicationService extends Service {
try {
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress);
if (btDevice.getName() == null || btDevice.getName().equals("MI")) { //FIXME: workaround for Miband not being paired
mGBDevice = new GBDevice(btDeviceAddress, "MI", GBDevice.Type.MIBAND);
mGBDevice = new GBDevice(btDeviceAddress, "MI", DeviceType.MIBAND);
mDeviceSupport = new MiBandSupport();
} else if (btDevice.getName().indexOf("Pebble") == 0) {
mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), GBDevice.Type.PEBBLE);
mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), DeviceType.PEBBLE);
mDeviceSupport = new PebbleSupport();
}
if (mDeviceSupport != null) {
mDeviceSupport.initialize(mGBDevice, mBtAdapter, this);
mDeviceSupport.connect();
if (pair) {
mDeviceSupport.pair();
} else {
mDeviceSupport.connect();
}
if (mDeviceSupport instanceof AbstractBTDeviceSupport) {
mGBDeviceIoThread = ((AbstractBTDeviceSupport) mDeviceSupport).getDeviceIOThread();
}

View File

@ -31,7 +31,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
if (deviceAddress != null) {
Intent connectIntent = new Intent(context, BluetoothCommunicationService.class);
connectIntent.setAction(BluetoothCommunicationService.ACTION_CONNECT);
connectIntent.putExtra("device_address", deviceAddress);
connectIntent.putExtra(BluetoothCommunicationService.EXTRA_DEVICE_ADDRESS, deviceAddress);
context.startService(connectIntent);
}
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
import nodomain.freeyourgadget.gadgetbridge.discovery.DiscoveryActivity;
public class ControlCenter extends Activity {
@ -83,7 +84,7 @@ public class ControlCenter extends Activity {
} else {
Intent startIntent = new Intent(ControlCenter.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CONNECT);
startIntent.putExtra("device_address", deviceList.get(position).getAddress());
startIntent.putExtra(BluetoothCommunicationService.EXTRA_DEVICE_ADDRESS, deviceList.get(position).getAddress());
startService(startIntent);
}
}
@ -152,6 +153,11 @@ public class ControlCenter extends Activity {
return true;
case R.id.action_refresh:
refreshPairedDevices();
return true;
case R.id.action_discover:
Intent discoverIntent = new Intent(this, DiscoveryActivity.class);
startActivity(discoverIntent);
return true;
}
return super.onOptionsItemSelected(item);
@ -182,24 +188,24 @@ public class ControlCenter extends Activity {
} else {
Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();
for (BluetoothDevice pairedDevice : pairedDevices) {
GBDevice.Type deviceType;
DeviceType deviceDeviceType;
if (pairedDevice.getName().indexOf("Pebble") == 0) {
deviceType = GBDevice.Type.PEBBLE;
deviceDeviceType = DeviceType.PEBBLE;
} else if (pairedDevice.getName().equals("MI")) {
deviceType = GBDevice.Type.MIBAND;
deviceDeviceType = DeviceType.MIBAND;
} else {
continue;
}
GBDevice device = new GBDevice(pairedDevice.getAddress(), pairedDevice.getName(), deviceType);
GBDevice device = new GBDevice(pairedDevice.getAddress(), pairedDevice.getName(), deviceDeviceType);
if (!availableDevices.contains(device)) {
availableDevices.add(device);
}
}
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
String miAddr = sharedPrefs.getString("development_miaddr", null);
String miAddr = sharedPrefs.getString(GB.PREF_DEVELOPMENT_MIBAND_ADDRESS, null);
if (miAddr != null && miAddr.length() > 0) {
GBDevice miDevice = new GBDevice(miAddr, "MI", GBDevice.Type.MIBAND);
GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND);
if (!availableDevices.contains(miDevice)) {
availableDevices.add(miDevice);
}

View File

@ -0,0 +1,15 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.app.Activity;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
public interface DeviceCoordinator {
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate.EXTRA_MAC_ADDRESS";
public boolean supports(DeviceCandidate candidate);
public boolean supports(GBDevice device);
public DeviceType getDeviceType();
Class<? extends Activity> getPairingActivity();
}

View File

@ -0,0 +1,80 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.bluetooth.BluetoothClass;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.pebble.PebbleCoordinator;
public class DeviceHelper {
private static DeviceHelper instance = new DeviceHelper();
public static DeviceHelper getInstance() {
return instance;
}
// lazily created
private List<DeviceCoordinator> coordinators;
// the current single coordinator (typically there's just one device connected
private DeviceCoordinator coordinator;
public boolean isSupported(DeviceCandidate candidate) {
if (coordinator != null && coordinator.supports(candidate)) {
return true;
}
for (DeviceCoordinator coordinator : getAllCoordinators()) {
if (coordinator.supports(candidate)) {
return true;
}
}
return false;
}
public DeviceCoordinator getCoordinator(DeviceCandidate device) {
if (coordinator != null && coordinator.supports(device)) {
return coordinator;
}
synchronized (this) {
for (DeviceCoordinator coord : getAllCoordinators()) {
if (coord.supports(device)) {
coordinator = coord;
return coordinator;
}
}
}
return new UnknownDeviceCoordinator();
}
public DeviceCoordinator getCoordinator(GBDevice device) {
if (coordinator != null && coordinator.supports(device)) {
return coordinator;
}
synchronized (this) {
for (DeviceCoordinator coord : getAllCoordinators()) {
if (coord.supports(device)) {
coordinator = coord;
return coordinator;
}
}
}
return new UnknownDeviceCoordinator();
}
public synchronized List<DeviceCoordinator> getAllCoordinators() {
if (coordinators == null) {
coordinators = createCoordinators();
}
return coordinators;
}
private List<DeviceCoordinator> createCoordinators() {
List<DeviceCoordinator> result = new ArrayList<>(2);
result.add(new MiBandCoordinator());
result.add(new PebbleCoordinator());
return result;
}
}

View File

@ -30,4 +30,6 @@ public interface DeviceSupport extends EventHandler {
public Context getContext();
public boolean useAutoConnect();
public void pair();
}

View File

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge;
public enum DeviceType {
UNKNOWN,
PEBBLE,
MIBAND
}

View File

@ -20,6 +20,8 @@ public class GB {
public static final int NOTIFICATION_ID = 1;
private static final String TAG = "GB";
public static final String PREF_DEVELOPMENT_MIBAND_ADDRESS = "development_miaddr";
public static Notification createNotification(String text, Context context) {
Intent notificationIntent = new Intent(context, ControlCenter.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
@ -81,4 +83,8 @@ public class GB {
}
return new String(hexChars);
}
public static String formatRssi(short rssi) {
return String.valueOf(rssi);
}
}

View File

@ -22,31 +22,35 @@ public class GBDevice implements Parcelable {
}
};
private static final String TAG = GBDevice.class.getSimpleName();
public static final short RSSI_UNKNOWN = 0;
public static final String EXTRA_DEVICE = "device";
private final String mName;
private final String mAddress;
private final Type mType;
private final DeviceType mDeviceType;
private String mFirmwareVersion = null;
private String mHardwareVersion = null;
private State mState = State.NOT_CONNECTED;
private short mBatteryLevel = 50; // unknown
private String mBatteryState;
private short mRssi = RSSI_UNKNOWN;
public GBDevice(String address, String name, Type type) {
public GBDevice(String address, String name, DeviceType deviceType) {
mAddress = address;
mName = name;
mType = type;
mDeviceType = deviceType;
validate();
}
private GBDevice(Parcel in) {
mName = in.readString();
mAddress = in.readString();
mType = Type.values()[in.readInt()];
mDeviceType = DeviceType.values()[in.readInt()];
mFirmwareVersion = in.readString();
mHardwareVersion = in.readString();
mState = State.values()[in.readInt()];
mBatteryLevel = (short) in.readInt();
mBatteryState = in.readString();
mRssi = (short) in.readInt();
validate();
}
@ -54,12 +58,13 @@ public class GBDevice implements Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mAddress);
dest.writeInt(mType.ordinal());
dest.writeInt(mDeviceType.ordinal());
dest.writeString(mFirmwareVersion);
dest.writeString(mHardwareVersion);
dest.writeInt(mState.ordinal());
dest.writeInt(mBatteryLevel);
dest.writeString(mBatteryState);
dest.writeInt(mRssi);
}
private void validate() {
@ -137,14 +142,30 @@ public class GBDevice implements Parcelable {
}
}
public Type getType() {
return mType;
public DeviceType getType() {
return mDeviceType;
}
public void setRssi(short rssi) {
if (rssi < 0) {
Log.w(TAG, "illegal rssi value " + rssi + ", setting to RSSI_UNKNOWN");
mRssi = RSSI_UNKNOWN;
} else {
mRssi = rssi;
}
}
/**
* Returns the device specific signal strength value, or #RSSI_UNKNOWN
*/
public short getRssi() {
return mRssi;
}
// TODO: this doesn't really belong here
public void sendDeviceUpdateIntent(Context context) {
Intent deviceUpdateIntent = new Intent(ACTION_DEVICE_CHANGED);
deviceUpdateIntent.putExtra("device", this);
deviceUpdateIntent.putExtra(EXTRA_DEVICE, this);
LocalBroadcastManager.getInstance(context).sendBroadcast(deviceUpdateIntent);
}
@ -208,9 +229,4 @@ public class GBDevice implements Parcelable {
INITIALIZED
}
public enum Type {
UNKNOWN,
PEBBLE,
MIBAND
}
}

View File

@ -25,7 +25,7 @@ public class SettingsActivity extends PreferenceActivity {
return true;
}
});
final Preference developmentMiaddr = findPreference("development_miaddr");
final Preference developmentMiaddr = findPreference(GB.PREF_DEVELOPMENT_MIBAND_ADDRESS);
bindPreferenceSummaryToValue(developmentMiaddr);
developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {

View File

@ -0,0 +1,27 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.app.Activity;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
public class UnknownDeviceCoordinator implements DeviceCoordinator {
@Override
public boolean supports(DeviceCandidate candidate) {
return false;
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
}
@Override
public DeviceType getDeviceType() {
return DeviceType.UNKNOWN;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return ControlCenter.class;
}
}

View File

@ -0,0 +1,18 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AndroidPairingActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_android_pairing);
}
}

View File

@ -0,0 +1,68 @@
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GB;
import nodomain.freeyourgadget.gadgetbridge.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
public class DeviceCandidateAdapter extends ArrayAdapter<DeviceCandidate> {
private final Context context;
private final List<DeviceCandidate> deviceCandidates;
public DeviceCandidateAdapter(Context context, List<DeviceCandidate> deviceCandidates) {
super(context, 0, deviceCandidates);
this.context = context;
this.deviceCandidates = deviceCandidates;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
DeviceCandidate device = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.device_candidate_item, parent, false);
}
ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_candidate_image);
TextView deviceNameLabel = (TextView) view.findViewById(R.id.device_candidate_name);
TextView deviceAddressLabel = (TextView) view.findViewById(R.id.device_candidate_address);
String name = formatDeviceCandidate(device);
deviceNameLabel.setText(name);
deviceAddressLabel.setText(device.getMacAddress());
switch (device.getDeviceType()) {
case PEBBLE:
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
break;
case MIBAND:
deviceImageView.setImageResource(R.drawable.ic_device_miband);
break;
default:
deviceImageView.setImageResource(R.drawable.ic_launcher);
}
return view;
}
private String formatDeviceCandidate(DeviceCandidate device) {
if (device.getRssi() > GBDevice.RSSI_UNKNOWN) {
return context.getString(R.string.device_with_rssi, device.getName(), GB.formatRssi(device.getRssi()));
}
return device.getName();
}
}

View File

@ -19,7 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.AbstractDeviceSupport;
* @see BtLEQueue
*/
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback {
private static final String TAG = "AbstractBTLEDeviceSupport";
private static final String TAG = "AbstractBTLEDeviceSupp";
private BtLEQueue mQueue;
private HashMap<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;

View File

@ -0,0 +1,67 @@
package nodomain.freeyourgadget.gadgetbridge.discovery;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.Parcelable;
import nodomain.freeyourgadget.gadgetbridge.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
/**
*/
public class DeviceCandidate implements Parcelable {
private BluetoothDevice device;
private short rssi;
private DeviceType deviceType = DeviceType.UNKNOWN;
public DeviceCandidate(BluetoothDevice device, short rssi) {
this.device = device;
this.rssi = rssi;
}
private DeviceCandidate(Parcel in) {
device = in.readParcelable(getClass().getClassLoader());
rssi = (short) in.readInt();
deviceType = DeviceType.valueOf(in.readString());
if (device == null || deviceType == null) {
throw new IllegalStateException("Unable to read state from Parcel");
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(device, 0);
dest.writeInt(rssi);
dest.writeString(deviceType.name());
}
public DeviceType getDeviceType() {
return deviceType;
}
public String getMacAddress() {
return device != null ? device.getAddress() : GBApplication.getContext().getString(R.string._unknown_);
}
public String getName() {
String name = null;
if (device != null) {
name = device.getName();
}
if (name == null || name.length() == 0) {
name = GBApplication.getContext().getString(R.string._unknown_);
}
return name;
}
public short getRssi() {
return rssi;
}
@Override
public int describeContents() {
return 0;
}
}

View File

@ -0,0 +1,227 @@
package nodomain.freeyourgadget.gadgetbridge.discovery;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
public class DiscoveryActivity extends Activity implements AdapterView.OnItemClickListener {
private static final String TAG = "DiscoveryAct";
private BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
discoveryStarted();
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
discoveryFinished();
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
bluetoothStateChanged(oldState, newState);
break;
case BluetoothDevice.ACTION_FOUND:
break;
}
}
};
private void bluetoothStateChanged(int oldState, int newState) {
discoveryFinished();
startButton.setEnabled(newState == BluetoothAdapter.STATE_ON);
}
private void discoveryFinished() {
isScanning = false;
progressView.setVisibility(View.GONE);
startButton.setText(getString(R.string.discovery_start_scanning));
}
private void discoveryStarted() {
isScanning = true;
progressView.setVisibility(View.VISIBLE);
startButton.setText(getString(R.string.discovery_stop_scanning));
}
private ProgressBar progressView;
private BluetoothAdapter adapter;
private ArrayList<DeviceCandidate> deviceCandidates = new ArrayList<>();
private ListView deviceCandidatesView;
private DeviceCandidateAdapter cadidateListAdapter;
private Button startButton;
private boolean isScanning;
private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
DeviceCandidate candidate = new DeviceCandidate(device, (short) rssi);
if (DeviceHelper.getInstance().isSupported(candidate)) {
deviceCandidates.add(candidate);
cadidateListAdapter.notifyDataSetChanged();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_discovery);
startButton = (Button) findViewById(R.id.discovery_start);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onStartButtonClick(startButton);
}
});
progressView = (ProgressBar) findViewById(R.id.discovery_progressbar);
progressView.setProgress(0);
progressView.setIndeterminate(true);
progressView.setVisibility(View.GONE);
deviceCandidatesView = (ListView) findViewById(R.id.discovery_deviceCandidatesView);
cadidateListAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
deviceCandidatesView.setAdapter(cadidateListAdapter);
deviceCandidatesView.setOnItemClickListener(this);
IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(bluetoothReceiver, bluetoothIntents);
startDiscovery();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
ArrayList<Parcelable> restoredCandidates = savedInstanceState.getParcelableArrayList("deviceCandidates");
if (restoredCandidates != null) {
deviceCandidates.clear();
for (Parcelable p : restoredCandidates) {
deviceCandidates.add((DeviceCandidate) p);
}
}
}
public void onStartButtonClick(View button) {
Log.d(TAG, "Start Button clicked");
if (isScanning) {
stopDiscovery();
} else {
startDiscovery();
}
}
@Override
protected void onDestroy() {
unregisterReceiver(bluetoothReceiver);
super.onDestroy();
}
/**
* Pre: bluetooth is available, enabled and scanning is off
*/
private void startDiscovery() {
Log.i(TAG, "Starting discovery...");
discoveryStarted(); // just to make sure
if (ensureBluetoothReady()) {
startBLEDiscovery();
} else {
discoveryFinished();
Toast.makeText(this, "Enable Bluetooth to discover devices.", Toast.LENGTH_LONG).show();
}
}
private void stopDiscovery() {
if (isScanning) {
adapter.stopLeScan(leScanCallback);
// unfortunately, we never get a call back when stopping the scan, so
// we do it manually:
discoveryFinished();
}
}
private boolean ensureBluetoothReady() {
boolean available = checkBluetoothAvailable();
startButton.setEnabled(available);
if (available) {
adapter.cancelDiscovery();
// must not return the result of cancelDiscovery()
// appears to return false when currently not scanning
return true;
}
return false;
}
private boolean checkBluetoothAvailable() {
BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
if (bluetoothService == null) {
Log.w(TAG, "No bluetooth available");
this.adapter = null;
return false;
}
BluetoothAdapter adapter = bluetoothService.getAdapter();
if (!adapter.isEnabled()) {
Log.w(TAG, "Bluetooth not enabled");
this.adapter = null;
return false;
}
this.adapter = adapter;
return true;
}
private void startBLEDiscovery() {
adapter.startLeScan(leScanCallback);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
Log.e(TAG, "Device candidate clicked, but item not found");
return;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
Intent intent = new Intent(this, coordinator.getPairingActivity());
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS, deviceCandidate.getMacAddress());
startActivity(intent);
}
}

View File

@ -0,0 +1,30 @@
package nodomain.freeyourgadget.gadgetbridge.miband;
import android.app.Activity;
import nodomain.freeyourgadget.gadgetbridge.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
public class MiBandCoordinator implements DeviceCoordinator {
@Override
public boolean supports(DeviceCandidate candidate) {
return candidate.getMacAddress().toUpperCase().startsWith(MiBandService.MAC_ADDRESS_FILTER);
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
}
@Override
public DeviceType getDeviceType() {
return DeviceType.MIBAND;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return MiBandPairingActivity.class;
}
}

View File

@ -0,0 +1,104 @@
package nodomain.freeyourgadget.gadgetbridge.miband;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
import nodomain.freeyourgadget.gadgetbridge.BluetoothCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.GB;
import nodomain.freeyourgadget.gadgetbridge.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.discovery.DiscoveryActivity;
public class MiBandPairingActivity extends Activity {
private TextView message;
private boolean isPairing;
private String macAddress;
private BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (macAddress.equals(device.getAddress()) && device.isInitialized()) {
pairingFinished(true);
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mi_band_pairing);
message = (TextView) findViewById(R.id.miband_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
if (macAddress == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, DiscoveryActivity.class));
finish();
return;
}
startPairing();
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
if (isPairing) {
stopPairing();
}
super.onDestroy();
}
private void startPairing() {
isPairing = true;
message.setText(getString(R.string.miband_pairing, macAddress));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
Intent serviceIntent = new Intent(this, BluetoothCommunicationService.class);
serviceIntent.setAction(BluetoothCommunicationService.ACTION_START);
startService(serviceIntent);
serviceIntent = new Intent(this, BluetoothCommunicationService.class);
serviceIntent.setAction(BluetoothCommunicationService.ACTION_CONNECT);
serviceIntent.putExtra(BluetoothCommunicationService.EXTRA_PERFORM_PAIR, true);
serviceIntent.putExtra(BluetoothCommunicationService.EXTRA_DEVICE_ADDRESS, macAddress);
startService(serviceIntent);
}
private void pairingFinished(boolean pairedSuccessfully) {
isPairing = false;
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
if (pairedSuccessfully) {
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
sharedPrefs.edit().putString(GB.PREF_DEVELOPMENT_MIBAND_ADDRESS, macAddress).apply();
}
Intent intent = new Intent(this, ControlCenter.class);
startActivity(intent);
finish();
}
private void stopPairing() {
// TODO
isPairing = false;
}
}

View File

@ -250,7 +250,7 @@ public class MiBandService {
MIBAND_DEBUG.put(UUID.fromString("00002a48-0000-1000-8000-00805f9b34fb"), "Supported Unread Alert Category");
MIBAND_DEBUG.put(UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"), "System ID");
MIBAND_DEBUG.put(UUID.fromString("00002a1c-0000-1000-8000-00805f9b34fb"), "Temperature Measurement");
MIBAND_DEBUG.put(UUID.fromString("00002a1d-0000-1000-8000-00805f9b34fb"), "Temperature Type");
MIBAND_DEBUG.put(UUID.fromString("00002a1d-0000-1000-8000-00805f9b34fb"), "Temperature DeviceType");
MIBAND_DEBUG.put(UUID.fromString("00002a12-0000-1000-8000-00805f9b34fb"), "Time Accuracy");
MIBAND_DEBUG.put(UUID.fromString("00002a13-0000-1000-8000-00805f9b34fb"), "Time Source");
MIBAND_DEBUG.put(UUID.fromString("00002a16-0000-1000-8000-00805f9b34fb"), "Time Update Control Point");

View File

@ -32,6 +32,15 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
return true;
}
@Override
public void pair() {
for (int i = 0; i < 5; i++) {
if (connect()) {
return;
}
}
}
private byte[] getDefaultNotification() {
final int vibrateTimes = 1;
final long vibrateDuration = 250l;

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.pebble;
import android.app.Activity;
import nodomain.freeyourgadget.gadgetbridge.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.discovery.DeviceCandidate;
public class PebbleCoordinator implements DeviceCoordinator {
@Override
public boolean supports(DeviceCandidate candidate) {
return false;
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
}
@Override
public DeviceType getDeviceType() {
return DeviceType.PEBBLE;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return ControlCenter.class;
}
}

View File

@ -0,0 +1,12 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.AndroidPairingActivity">
<TextView android:text="@string/android_pairing_hint" android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>

View File

@ -0,0 +1,36 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.discovery.DiscoveryActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/discovery_start_scanning"
android:id="@+id/discovery_start"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/discovery_progressbar"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"
android:layout_below="@+id/discovery_start"
android:layout_centerHorizontal="true" />
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/discovery_deviceCandidatesView"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@+id/discovery_progressbar"
android:layout_below="@+id/discovery_progressbar"
android:layout_alignParentStart="true" />
</RelativeLayout>

View File

@ -0,0 +1,29 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.miband.MiBandPairingActivity">
<TextView android:text="@string/miband_pairing" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/miband_pair_message" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_marginTop="25dp"
android:layout_below="@+id/miband_pair_message"
android:layout_centerHorizontal="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="When your Mi Band vibrates and blinks, tap it a few times in a row."
android:id="@+id/miband_pair_hint"
android:layout_centerVertical="true"
android:layout_alignParentStart="true" />
</RelativeLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:padding="8dp" >
<ImageView
android:id="@+id/device_candidate_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentLeft="true" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/device_candidate_image"
android:orientation="vertical"
android:paddingLeft="8dp" >
<TextView
android:id="@+id/device_candidate_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scrollHorizontally="false"
android:textStyle="bold"
android:singleLine="true" />
<TextView
android:id="@+id/device_candidate_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="normal" />
</LinearLayout>
</RelativeLayout>

View File

@ -2,6 +2,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="nodomain.freeyourgadget.gadgetbridge.ControlCenter">
<item android:id="@+id/action_discover" android:title="@string/action_discover"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_refresh" android:title="@string/action_refresh"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_settings" android:title="@string/action_settings"

View File

@ -39,7 +39,7 @@
<string name="never">never</string>
<string name="pref_header_development">Developer Options</string>
<string name="pref_title_development_miaddr">Miband address</string>
<string name="pref_title_development_miaddr">Mi Band address</string>
<string name="not_connected">not connected</string>
@ -70,4 +70,15 @@
<string name="n_a">N/A</string>
<string name="initialized">initialized</string>
<string name="appversion_by_creator">%1$s by %2$s</string>
<string name="title_activity_discovery">Device Discovery</string>
<string name="discovery_stop_scanning">Stop Scanning</string>
<string name="discovery_start_scanning">Start Discovery</string>
<string name="action_discover">Discover Device</string>
<string name="device_with_rssi">%1$s (%2$s)</string>
<string name="title_activity_android_pairing">Pair Device</string>
<string name="android_pairing_hint">Use the Android Bluetooth Pairing dialog to pair the device.</string>
<string name="title_activity_mi_band_pairing">Pair your Mi Band</string>
<string name="miband_pairing">Pairing with %s…</string>
<string name="message_cannot_pair_no_mac">No mac address passed, cannot pair.</string>
</resources>