blob: d286cf177973aba06b47bb1e7a05771c5a0a8559 [file] [log] [blame]
/*
* Copyright (C) 2015 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.dragonkeyboardfirmwareupdater;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import java.lang.System;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
public class KeyboardFirmwareUpdateService extends Service {
private static final String TAG = KeyboardFirmwareUpdateService.class.getSimpleName();
/* Actions for update status changes. */
public static final String ACTION_KEYBOARD_UPDATE_CONFIRMED =
"com.android.dragonkeyboardfirmwareupdater.action.KEYBOARD_UPDATE_CONFIRMED";
public static final String ACTION_KEYBOARD_UPDATE_POSTPONED =
"com.android.dragonkeyboardfirmwareupdater.action.KEYBOARD_UPDATE_POSTPONED";
/* Extra information for UpdaterConfirmationActivity. */
public static final String EXTRA_KEYBOARD_NAME =
"com.android.dragonkeyboardfirmwareupdater.EXTRA_KEYBOARD_NAME";
public static final String EXTRA_KEYBOARD_ADDRESS =
"com.android.dragonkeyboardfirmwareupdater.EXTRA_KEYBOARD_ADDRESS";
public static final String EXTRA_KEYBOARD_FIRMWARE_VERSION =
"com.android.dragonkeyboardfirmwareupdater.EXTRA_KEYBOARD_FIRMWARE_VERSION";
public static final String PREFERENCE_NEXT_UPDATE_TIME =
"com.android.dragonkeyboardfirmwareupdater.PREFERENCE_NEXT_UPDATE_TIME";
/**
* Bluetooth connectivity. The Bluetooth LE connection maintained in this service is for
* retrieving keyboard information, such as device manufacture and so on, and switching the
* keyboard to Device Update Mode (DFU). Once the DFU service is started, the connection
* maintained here and its corresponding variable should be cleaned up.
*/
// Bluetooth Gatt connection state of the keyboard.
private static final int GATT_STATE_DISCONNECTED = 0;
private static final int GATT_STATE_CONNECTING = 1;
private static final int GATT_STATE_CONNECTED = 2;
private static final int GATT_STATE_DISCOVERING_SERVICES = 3;
private static final int GATT_STATE_DISCONNECTING = 4;
// Bluetooth system services.
private static final long SCAN_PERIOD = 7000; // 7 seconds
private final IBinder mBinder = new LocalBinder();
private int mGattConnectionState = GATT_STATE_DISCONNECTED;
private int mGattOperationStatus = -1;
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothLeScanner mBluetoothLeScanner;
// Bluetooth Gatt connection bond with the keyboard.
private BluetoothGatt mBluetoothGattClient;
private String mKeyboardName;
private String mKeyboardAddress;
private String mKeyboardAddressInAppMode;
private String mKeyboardFirmwareVersion;
// Bluetooth Gatt services provided by the keyboard.
private BluetoothGattService mBatteryService;
private BluetoothGattService mDeviceInfoService;
private BluetoothGattService mDfuService;
private BluetoothGattCharacteristic mDfuChar;
// Bluetooth LE scan retry flag.
private boolean mLeScanRetried = false;
private int mDfuStatus = DFU_STATE_NOT_STARTED;
/* Handler for posting delayed tasks. */
private Handler mHandler;
/* Wake lock for DFU. */
private PowerManager.WakeLock mWakeLock;
/* Update notification. */
private static final int UPDATE_NOTIFICATION_ID = 1248;
private static final int BATTERY_WARNING_NOTIFICATION_ID = 8421;
private Notification mUpdateNotification;
/**
* Keeps track of the status of DFU process.
* DFU_STATE_NOT_STARTED: DFU not started
* DFU_STATE_OBTAINING_INFO: DFU not started, obtaining manufacturer, firmware version and battery level
* DFU_STATE_INFO_READY: DFU not started, manufacturer, firmware version and battery level obtained
* DFU_STATE_SWITCHING_TO_DFU_MODE: DFU not started, waiting for the keyboard to reboot into DFU mode
* DFU_STATE_MODE_SWITCHED: DFU not started, switched to DFU mode
* DFU_STATE_UPDATING: DFU started, pushing the new firmware to the keyboard
* DFU_STATE_UPDATE_COMPLETE: DFU finished correctly
* DFU_STATE_INFO_NOT_SUITABLE: DFU not started, the keyboard is not suitable for update
* DFU_STATE_OBTAIN_INFO_ERROR: DFU not started, error(s) occurred during obtaining information
* DFU_STATE_SWITCH_TO_DFU_MODE_ERROR: DFU not started, failed to switch to DFU mode
* DFU_STATE_UPDATE_ABORTED: DFU started but aborted by either users or errors during update
*/
private static final int DFU_STATE_NOT_STARTED = 5;
private static final int DFU_STATE_OBTAINING_INFO = 6;
private static final int DFU_STATE_INFO_READY = 7;
private static final int DFU_STATE_SWITCHING_TO_DFU_MODE = 8;
private static final int DFU_STATE_MODE_SWITCHED = 9;
private static final int DFU_STATE_UPDATING = 10;
private static final int DFU_STATE_UPDATE_COMPLETE = 11;
private static final int DFU_STATE_INFO_NOT_SUITABLE = 12;
private static final int DFU_STATE_OBTAIN_INFO_ERROR = 13;
private static final int DFU_STATE_SWITCH_TO_DFU_MODE_ERROR = 14;
private static final int DFU_STATE_UPDATE_ABORTED = 15;
/* Handles Bluetooth LE scan results. Bluetooth LE scan occurs after DFU preparation is ready. */
private ScanCallback mBluetoothLeScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
if (device == null || device.getName() == null) return;
// Find the keyboard in DFU mode and start DFU process. The name of keyboard in DFU mode
// is composed of the last three groups (G3:G4:G5) of its application mode address
// (G0:G1:G2:G3:G4:G5).
if (mKeyboardAddressInAppMode.endsWith(device.getName().toUpperCase()) &&
mDfuStatus != DFU_STATE_UPDATING) {
Log.d(TAG, "onScanResult: Found target keyboard in DFU mode");
scanLeDevice(false);
// Start pushing new firmware to the keyboard.
startDfuService(device.getName(), device.getAddress());
}
}
@Override
public void onScanFailed(int errorCode) {
Log.w(TAG, "onScanFailed: Error Code: " + errorCode);
if (!mLeScanRetried) {
// Retry the scan once.
mLeScanRetried = true;
scanLeDevice(true);
}
}
};
/**
* Handles Bluetooth Gatt client callback. Read/write operations should finish in a certain
* order after DFU preparation starts.
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
private boolean retryFlag = true;
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
assert gatt == mBluetoothGattClient;
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
if (!checkOperationStatus(status)) {
Log.w(TAG, "BluetoothGattCallback: Transferring to new state: " + newState +
" failed with code: " + status);
return;
}
changeGattState(GATT_STATE_CONNECTED);
Log.i(TAG, "BluetoothGattCallback: Connected to Bluetooth Gatt server on " +
getKeyboardString());
// Start to discover services right after connection.
mBluetoothGattClient.discoverServices();
changeGattState(GATT_STATE_DISCOVERING_SERVICES);
Log.d(TAG, "BluetoothGattCallback: Start to discover services on " +
getKeyboardString());
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.i(TAG, "BluetoothGattCallback: Disconnected from Bluetooth Gatt server on "
+ getKeyboardString() + ", status: " + status);
if (mGattConnectionState != GATT_STATE_DISCONNECTED) {
changeGattState(GATT_STATE_DISCONNECTED);
}
break;
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
assert gatt == mBluetoothGattClient;
if (!checkOperationStatus(status)) {
changeDfuStatus(DFU_STATE_NOT_STARTED);
return;
}
if (!getGattServices()) return;
changeGattState(GATT_STATE_CONNECTED);
readBatteryLevel();
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
assert gatt == mBluetoothGattClient;
if (!checkOperationStatus(status)) {
changeDfuStatus(DFU_STATE_OBTAIN_INFO_ERROR);
return;
}
UUID uuid = characteristic.getUuid();
if (GattAttributeUUID.UUID_BATTERY_LEVEL_CHARACTERISTIC.equals(uuid)) {
int batteryLevel = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
if (batteryLevel < Integer.parseInt(getString(R.string.target_battery_level))) {
Log.w(TAG, "onCharacteristicRead BATTERY_LEVEL_CHARACTERISTIC: " +getKeyboardString()
+ " battery level(" + batteryLevel + "%) is too low:");
changeDfuStatus(DFU_STATE_INFO_NOT_SUITABLE);
showBatteryWarningNotification();
return;
}
readDeviceManufacturer();
} else if (GattAttributeUUID.UUID_DEVICE_INFORMATION_MANUFACTURER_CHARACTERISTIC.equals(uuid)) {
String manufacturer = new String(characteristic.getValue());
if (!manufacturer.equals(getString(R.string.target_manufacturer))) {
Log.d(TAG, "onCharacteristicRead DEVICE_INFORMATION_MANUFACTURER_CHARACTERISTIC: Invalid manufacturer: "
+ manufacturer);
changeDfuStatus(DFU_STATE_INFO_NOT_SUITABLE);
return;
}
readDeviceFirmwareVersion();
} else if (GattAttributeUUID.UUID_DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC.equals(uuid)) {
mKeyboardFirmwareVersion = new String(characteristic.getValue());
Log.d(TAG, "onCharacteristicRead DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC: current: "
+ mKeyboardFirmwareVersion + " new: " + getString(R.string.target_firmware_version));
Float versionNumber = 0.0f;
// Parse the firmware version into Float number for the following checks.
try {
versionNumber = Float.parseFloat(mKeyboardFirmwareVersion);
} catch(NumberFormatException e) {
Log.w(TAG, "onCharacteristicRead DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC: " +
"firmware version parsing error");
changeDfuStatus(DFU_STATE_INFO_NOT_SUITABLE);
return;
}
// Check if the current firmware is updatable.
if (versionNumber < Float.parseFloat(getString(R.string.target_min_updatable_firmware_version))) {
Log.d(TAG, "onCharacteristicRead DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC: " +
"current firmware(" + mKeyboardFirmwareVersion + ") is not updatable");
changeDfuStatus(DFU_STATE_INFO_NOT_SUITABLE);
return;
}
// Check if the current firmware is up to date.
if (versionNumber >= Float.parseFloat(getString(R.string.target_firmware_version))) {
Log.d(TAG, "onCharacteristicRead DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC: " +
getKeyboardString() + " firmware(" + mKeyboardFirmwareVersion + ") is up to date");
changeDfuStatus(DFU_STATE_INFO_NOT_SUITABLE);
return;
}
changeDfuStatus(DFU_STATE_INFO_READY);
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
assert gatt == mBluetoothGattClient;
if (GattAttributeUUID.UUID_DFU_CONTROL_POINT_CHARACTERISTIC.equals(characteristic.getUuid())) {
Log.d(TAG, "onCharacteristicWrite DFU_CONTROL_POINT_CHARACTERISTIC: status: " + status);
changeDfuStatus(DFU_STATE_MODE_SWITCHED);
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
assert gatt == mBluetoothGattClient;
if (GattAttributeUUID.UUID_DFU_CONTROL_POINT_DESCRIPTOR.equals(descriptor.getUuid())) {
Log.d(TAG, "onDescriptorWrite: DFU_CONTROL_POINT_DESCRIPTOR, status: " + status);
enableDfuMode();
}
}
};
/* Handles DfuService callback. DFU service starts after the keyboard is found in LE scan.*/
private DfuProgressListener mDfuProgressListener = new DfuProgressListener() {
private void cancelDfuServiceNotification() {
// Wait a bit before cancelling notification.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// If this activity is still open and upload process was completed, cancel the notification.
final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(DfuService.NOTIFICATION_ID);
}
}, 200);
}
@Override
public void onDeviceConnecting(String deviceAddress) {
}
@Override
public void onDeviceConnected(String deviceAddress) {
}
@Override
public void onDfuProcessStarting(String deviceAddress) {
}
@Override
public void onDfuProcessStarted(String deviceAddress) {
}
@Override
public void onEnablingDfuMode(String deviceAddress) {
}
@Override
public void onProgressChanged(
String deviceAddress, int percent, float speed, float avgSpeed, int currentPart, int partsTotal) {
if ((percent % 5) == 0) {
Log.i(TAG, "DfuProgressListener: onProgressChanged: part" + currentPart + "/" +
partsTotal + ", " + percent + "%");
}
}
@Override
public void onFirmwareValidating(String deviceAddress) {
}
@Override
public void onDeviceDisconnecting(String deviceAddress) {
}
@Override
public void onDeviceDisconnected(String deviceAddress) {
}
@Override
public void onDfuCompleted(String deviceAddress) {
Log.d(TAG, "DfuProgressListener: onDfuCompleted");
cancelDfuServiceNotification();
changeDfuStatus(DFU_STATE_UPDATE_COMPLETE);
}
@Override
public void onDfuAborted(String deviceAddress) {
Log.e(TAG, "DfuProgressListener: onDfuAborted");
cancelDfuServiceNotification();
changeDfuStatus(DFU_STATE_UPDATE_ABORTED);
}
@Override
public void onError(String deviceAddress, int error, int errorType, String message) {
Log.e(TAG, "DfuProgressListener: onError: " + message);
cancelDfuServiceNotification();
changeDfuStatus(DFU_STATE_UPDATE_ABORTED);
}
};
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onHandleIntent(context, intent);
}
};
/* Dynamically creates intent filter for BroadcastReceiver. */
private static IntentFilter makeIntentFilter() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
intentFilter.addAction(ACTION_KEYBOARD_UPDATE_CONFIRMED);
intentFilter.addAction(ACTION_KEYBOARD_UPDATE_POSTPONED);
return intentFilter;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: " + getString(R.string.app_name));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand: " + getString(R.string.app_name));
enableBluetoothConnectivity();
DfuServiceListenerHelper.registerProgressListener(this, mDfuProgressListener);
registerReceiver(mBroadcastReceiver, makeIntentFilter());
mHandler = new Handler();
final String deviceName = intent.getStringExtra(EXTRA_KEYBOARD_NAME);
final String deviceAddress = intent.getStringExtra(EXTRA_KEYBOARD_ADDRESS);
Log.d(TAG, "onStartCommand: device " + deviceName + "[" + deviceAddress + "]");
if (isUpdateServiceInUse() || deviceAddress == null ||
!getString(R.string.target_keyboard_name).equals(deviceName)) {
terminateSelf();
return START_NOT_STICKY;
}
// Check the next update time.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final long nextUpdateTime = preferences.getLong(PREFERENCE_NEXT_UPDATE_TIME + deviceAddress, 0);
if (nextUpdateTime != 0 && System.currentTimeMillis() < nextUpdateTime) {
SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss.SSS");
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(nextUpdateTime);
Log.d(TAG, "onStartCommand: next update time: " + formatter.format(calendar.getTime()));
terminateSelf();
return START_NOT_STICKY;
}
if (nextUpdateTime != 0) {
SharedPreferences.Editor editor = preferences.edit();
editor.remove(PREFERENCE_NEXT_UPDATE_TIME + deviceAddress);
editor.apply();
}
obtainKeyboardInfo(deviceName, deviceAddress);
if (mDfuStatus != DFU_STATE_INFO_READY) {
Log.w(TAG, "onHandleIntent: DFU preparation failed");
changeDfuStatus(DFU_STATE_OBTAIN_INFO_ERROR);
return START_NOT_STICKY;
}
// TODO(mcchou): Return proper flag.
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
dismissUpdateNotification();
handleGattAndDfuCleanup();
cleanUpGattConnection();
disableBluetoothConnectivity();
DfuServiceListenerHelper.unregisterProgressListener(this, mDfuProgressListener);
unregisterReceiver(mBroadcastReceiver);
Log.d(TAG, "onDestroy: " + getString(R.string.app_name));
}
/* Terminates the service. */
private void terminateSelf() {
Log.d(TAG, "terminateSelf: DFU status: " + getDfuStateString(mDfuStatus));
stopSelf();
}
/**
* Handles intents ACTION_CONNECTION_STATE_CHANGED, ACTION_STATE_CHANGED,
* ACTION_BOND_STATE_CHANGED, ACTION_KEYBOARD_UPDATE_CONFIRMED.
* <p/>
* [ACTION_STATE_CHANGED]
* This action is used to keep track of ON/OFF state change on the system Bluetooth adapter.
* The
* purpose is to synchronize the local Bluetooth connectivity with system Bluetooth state.
* <p/>
* [ACTION_CONNECTION_STATE_CHANGED]
* This action is used to keep track of the connection change on the target device. The purpose
* is to synchronize the connection cycles of the local GATT connection and the system
* Bluetooth
* connection.
* <p/>
* [ACTION_BOND_STATE_CHANGED]
* This action is used to keep track of the bond state change on the target device. The purpose
* is to the connection cycles of the local GATT connection and the system Bluetooth
* connection.
* <p/>
* [ACTION_KEYBOARD_UPDATE_CONFIRMED]
* This action is used to receive the update confirmation from the user. The purpose is to
* trigger DFU process.
*/
private void onHandleIntent(Context context, Intent intent) {
final String action = intent.getAction();
Log.d(TAG, "onHandleIntent: Received action: " + action);
if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
if (!isBluetoothEnabled()) {
Log.w(TAG, "onHandleIntent: Bluetooth connectivity not enabled");
return;
}
// Match the connected device with the default keyboard name.
Bundle extras = intent.getExtras();
if (extras == null) return;
final BluetoothDevice device = extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
final int deviceConnectionState = extras.getInt(BluetoothAdapter.EXTRA_CONNECTION_STATE);
Log.d(TAG, "onHandleIntent: " + device.getName() + " [" + device.getAddress() +
"] change to state: " + deviceConnectionState);
if (!isTargetKeyboard(device)) return;
if (deviceConnectionState == BluetoothAdapter.STATE_DISCONNECTED) {
if (mDfuStatus != DFU_STATE_SWITCHING_TO_DFU_MODE && mDfuStatus != DFU_STATE_MODE_SWITCHED) {
terminateSelf();
}
}
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
final int adapterState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
if (adapterState == BluetoothAdapter.STATE_TURNING_OFF) {
// Terminate update process and disable Bluetooth connectivity.
// Since BluetoothAdapter has been disabled, the callback of disconnection would not
// be called. Therefore a separate clean-up of GATT connection is need.
terminateSelf();
}
} else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras == null) return;
final BluetoothDevice device = extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
final int deviceBondState = extras.getInt(BluetoothDevice.EXTRA_BOND_STATE);
Log.d(TAG, "onHandleIntent: state change on device " + device.getName() + " [" +
device.getAddress() + "], bond state: " + deviceBondState);
if (!isTargetKeyboard(device)) return;
if (deviceBondState == BluetoothDevice.BOND_NONE) {
terminateSelf();
}
} else if (ACTION_KEYBOARD_UPDATE_CONFIRMED.equals(action)) {
dismissUpdateNotification();
// Check if the incoming update confirmation is associated with the current keyboard.
String keyboardName = intent.getStringExtra(EXTRA_KEYBOARD_NAME);
String keyboardAddress = intent.getStringExtra(EXTRA_KEYBOARD_ADDRESS);
if (!mKeyboardName.equals(keyboardName) || !mKeyboardAddress.equals(keyboardAddress)) {
Log.w(TAG, "onHandleIntent: No DFU service associated with " + keyboardName + " [" +
keyboardAddress + "]");
return;
}
if (mDfuStatus != DFU_STATE_INFO_READY || mDfuStatus == DFU_STATE_UPDATING) {
Log.w(TAG, "onHandleIntent: DFP preparation not ready or DFU is in progress.");
changeDfuStatus(DFU_STATE_UPDATE_ABORTED);
return;
}
Log.d(TAG, "onHandleIntent: Start update process on " + keyboardName + " [" +
keyboardAddress + "]");
changeDfuStatus(DFU_STATE_SWITCHING_TO_DFU_MODE);
} else if (ACTION_KEYBOARD_UPDATE_POSTPONED.equals(action)) {
dismissUpdateNotification();
// TODO(mcchou): Update the preference when the Settings keyboard entry is available.
Log.d(TAG, "onHandleIntent: Postpone the update");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(PREFERENCE_NEXT_UPDATE_TIME + mKeyboardAddress,
System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS));
editor.apply();
}
}
/* Checks if Bluetooth connectivity is enabled. */
private boolean isBluetoothEnabled() {
return (mBluetoothManager != null && mBluetoothAdapter != null && mBluetoothLeScanner != null);
}
/* Checks if there is already a keyboard associated with the update service. */
private boolean isUpdateServiceInUse() {
return (mKeyboardName != null && mKeyboardAddress != null);
}
/* Returns a string including the keyboard name and address. */
private String getKeyboardString() {
return mKeyboardName + " [" + mKeyboardAddress + "]";
}
/* Checks if the device is the keyboard associated with the service. */
private boolean isTargetKeyboard(BluetoothDevice device) {
if (device == null) return false;
return (mKeyboardName.equals(device.getName()) && mKeyboardAddress.equals(device.getAddress()));
}
/* Retrieves Bluetooth manager, adapter and scanner. */
private boolean enableBluetoothConnectivity() {
Log.d(TAG, "EnableBluetoothConnectivity");
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.w(TAG, "EnableBluetoothConnectivity: Failed to obtain BluetoothManager");
return false;
}
}
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.w(TAG, "EnableBluetoothConnectivity: Failed to obtain BluetoothAdapter");
return false;
}
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
if (mBluetoothLeScanner == null) {
Log.w(TAG, "EnableBluetoothConnectivity: Failed to obtain BluetoothLeScanner");
return false;
}
return true;
}
/* Disables Bluetooth connectivity if exists. */
private void disableBluetoothConnectivity() {
Log.d(TAG, "disableBluetoothConnectivity");
mBluetoothManager = null;
mBluetoothAdapter = null;
mBluetoothLeScanner = null;
}
/* Shows the update notification. */
private void showUpdateNotification() {
Log.d(TAG, "showUpdateNotification: " + getKeyboardString());
// Intent for triggering the update confirmation page.
Intent updateConfirmation = new Intent(this, UpdateConfirmationActivity.class);
updateConfirmation.putExtra(EXTRA_KEYBOARD_NAME, mKeyboardName);
updateConfirmation.putExtra(EXTRA_KEYBOARD_ADDRESS, mKeyboardAddress);
updateConfirmation.putExtra(EXTRA_KEYBOARD_FIRMWARE_VERSION, mKeyboardFirmwareVersion);
updateConfirmation.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Intent for postponing update.
Intent postponeUpdate = new Intent(ACTION_KEYBOARD_UPDATE_POSTPONED);
// Wrap intents into pending intents for notification use.
PendingIntent laterIntent = PendingIntent.getBroadcast(
this, 0, postponeUpdate, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent installIntent = PendingIntent.getActivity(
this, 0, updateConfirmation, PendingIntent.FLAG_CANCEL_CURRENT);
// Create a notification object with two buttons (actions)
mUpdateNotification = new NotificationCompat.Builder(this)
.setCategory(Notification.CATEGORY_SYSTEM)
.setContentTitle(getString(R.string.notification_update_title))
.setContentText(getString(R.string.notification_update_text))
.setContentIntent(installIntent)
.setSmallIcon(R.drawable.ic_keyboard)
.addAction(new NotificationCompat.Action.Builder(
R.drawable.ic_later, getString(R.string.notification_update_later),
laterIntent).build())
.addAction(new NotificationCompat.Action.Builder(
R.drawable.ic_install, getString(R.string.notification_update_install),
installIntent).build())
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.build();
// Show the notification via notification manager
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(UPDATE_NOTIFICATION_ID, mUpdateNotification);
}
/* Dismisses the udpate notification. */
private void dismissUpdateNotification() {
if (mUpdateNotification == null) return;
Log.d(TAG, "dismissUpdateNotification");
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(UPDATE_NOTIFICATION_ID);
mUpdateNotification = null;
}
/* Shows the keyboard battery warning notification. */
private void showBatteryWarningNotification() {
Log.d(TAG, "showBatteryWarningNotification: " + getKeyboardString());
Notification batteryWarningNotification = new NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.notification_battery_warning_title))
.setContentText(getString(R.string.notification_battery_warning_text))
.setSmallIcon(R.drawable.ic_battery_warning)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(Notification.PRIORITY_HIGH)
.setColor(Color.RED)
.setStyle(new NotificationCompat.BigTextStyle().bigText(
getString(R.string.notification_battery_warning_text)))
.build();
// Show the notification via notification manager
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(BATTERY_WARNING_NOTIFICATION_ID, batteryWarningNotification);
}
/* Connects to the GATT server hosted on the given Bluetooth LE device. */
private boolean connectToKeyboard() {
if (!isBluetoothEnabled() || !isUpdateServiceInUse()) {
Log.w(TAG, "connectToKeyboard: Bluetooth connectivity not enabled or associated keyboard not found.");
return false;
}
final BluetoothDevice keyboard = mBluetoothAdapter.getRemoteDevice(mKeyboardAddress);
if (keyboard == null) {
Log.w(TAG, "connectToKeyboard: " + getKeyboardString() + " not found. Unable to connect.");
return false;
}
Log.d(TAG, "connectToKeyboard: Trying to create a new connection to " + getKeyboardString());
mBluetoothGattClient = keyboard.connectGatt(this, false, mGattCallback);
changeGattState(GATT_STATE_CONNECTING);
mGattOperationStatus = BluetoothGatt.GATT_SUCCESS;
return true;
}
/* Disconnects from the GATT server hosted on the given Bluetooth LE device. */
private void disconnectFromKeyboard() {
if (mGattConnectionState == GATT_STATE_DISCONNECTED) return;
if (!isUpdateServiceInUse() || !isBluetoothEnabled() || mDfuStatus == DFU_STATE_NOT_STARTED) {
Log.i(TAG, "disconnectFromKeyboard: Bluetooth connectivity not enabled");
return;
}
Log.d(TAG, "disconnectFromKeyboard: " + getKeyboardString());
mBluetoothGattClient.disconnect();
changeGattState(GATT_STATE_DISCONNECTING);
mGattOperationStatus = BluetoothGatt.GATT_SUCCESS;
// Wait 2 seconds for GATT disconnection request to finish.
try {
Thread.sleep(2000); // 2 seconds
Log.d(TAG, "disconnectFromKeyboard: Wait for GATT disconnection");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Cleans up Bluetooth GATT connection and the keyboard. This should be done before starting
* DFU process.
*/
private void cleanUpGattConnection() {
Log.d(TAG, "cleanUpGattConnection");
mKeyboardFirmwareVersion = null;
mBluetoothGattClient = null;
mBatteryService = null;
mDeviceInfoService = null;
mDfuService = null;
mDfuChar = null;
mLeScanRetried = false;
}
/* Starts to collect the information of the keyboard. */
private void obtainKeyboardInfo(String keyboardName, String keyboardAddress) {
Log.d(TAG, "obtainKeyboardInfo: Obtain the information of " + keyboardName + " [" +
keyboardAddress + "]");
// Connect to the keyboard and start to obtain its information.
mKeyboardName = keyboardName;
mKeyboardAddress = keyboardAddress.toUpperCase();
mKeyboardAddressInAppMode = mKeyboardAddress;
Log.d(TAG, "obtainKeyboardInfo: Associate DFU service with " + getKeyboardString());
if (mGattConnectionState == GATT_STATE_CONNECTED) {
Log.i(TAG, "obtainKeyboardInfo: Reuse previous GATT connection");
readBatteryLevel();
} else if (mGattConnectionState == GATT_STATE_DISCONNECTED) {
changeDfuStatus(DFU_STATE_OBTAINING_INFO);
} else {
Log.w(TAG, "obtainKeyboardInfo: Failed to obtain keyboard information");
}
// Wait at most 10 seconds for the queries to GATT attributes to finish.
int waitTimes = 5;
while (mDfuStatus == DFU_STATE_OBTAINING_INFO && waitTimes > 0) {
try {
Thread.sleep(2000); // 2 seconds
waitTimes--;
Log.d(TAG, "obtainKeyboardInfo: Wait for preparation completion");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/* Starts/stops Bluetooth LE scan. */
private void scanLeDevice(final boolean enable) {
if (!isBluetoothEnabled()) {
Log.w(TAG, "scanLeDevice: Bluetooth connectivity not enabled");
}
if (enable) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d(TAG, "scanLeDevice: Stop scanning");
mBluetoothLeScanner.stopScan(mBluetoothLeScanCallback);
}
}, SCAN_PERIOD);
Log.d(TAG, "scanLeDevice: Start scanning");
mBluetoothLeScanner.startScan(mBluetoothLeScanCallback);
} else {
Log.d(TAG, "scanLeDevice: Stop scanning");
mBluetoothLeScanner.stopScan(mBluetoothLeScanCallback);
}
}
/**
* Retrieves Battery Service, Device Information Service, Device Firmware Update(DFU) service
* and DFU Control Point Characteristic.
*/
private boolean getGattServices() {
Log.d(TAG, "getDfuServiceAndChar");
if (mBluetoothGattClient == null) {
Log.w(TAG, "getDfuServiceAndChar: Bluetooth GATT connection not initiated");
return false;
}
mBatteryService = mBluetoothGattClient.getService(GattAttributeUUID.UUID_BATTERY_SERVICE);
if (mBatteryService == null) {
Log.e(TAG, "getBatteryService: Failed to get Battery Service");
return false;
}
mDeviceInfoService = mBluetoothGattClient.getService(GattAttributeUUID.UUID_DEVICE_INFORMATION_SERVICE);
if (mDeviceInfoService == null) {
Log.e(TAG, "getDeviceInfoService: Failed to get Device Information Service");
return false;
}
mDfuService = mBluetoothGattClient.getService(GattAttributeUUID.UUID_DFU_SERVICE);
if (mDfuService == null) {
Log.e(TAG, "getDfuServiceAndChar: Failed to get Device Firmware Update Service");
return false;
}
mDfuChar = mDfuService.getCharacteristic(GattAttributeUUID.UUID_DFU_CONTROL_POINT_CHARACTERISTIC);
if (mDfuChar == null) {
Log.e(TAG, "getDfuServiceAndChar: Failed to get DFU Control Point characteristic");
return false;
}
return true;
}
/* Retrieves battery level of the connected keyboard. */
private boolean readBatteryLevel() {
Log.d(TAG, "readBatteryLevel");
BluetoothGattCharacteristic batteryLevelChar = mBatteryService.getCharacteristic(
GattAttributeUUID.UUID_BATTERY_LEVEL_CHARACTERISTIC);
if (batteryLevelChar == null || !mBluetoothGattClient.readCharacteristic(batteryLevelChar)) {
Log.e(TAG, "readBatteryLevel: Failed to init batter level read operation");
return false;
}
return true;
}
/* Retrieves device manufacturer of the connected keyboard. */
private boolean readDeviceManufacturer() {
Log.d(TAG, "readDeviceManufacturer");
BluetoothGattCharacteristic deviceManufacturerChar = mDeviceInfoService.getCharacteristic(
GattAttributeUUID.UUID_DEVICE_INFORMATION_MANUFACTURER_CHARACTERISTIC);
if (deviceManufacturerChar == null ||
!mBluetoothGattClient.readCharacteristic(deviceManufacturerChar)) {
Log.e(TAG, "readDeviceInfo: Failed to init device manufacturer characteristic read operation");
return false;
}
return true;
}
/* Retrieves device firmware version of the connected keyboard. */
private boolean readDeviceFirmwareVersion() {
Log.d(TAG, "readDeviceFirmwareVersion");
BluetoothGattCharacteristic deviceFirmwareVersionChar = mDeviceInfoService.getCharacteristic(
GattAttributeUUID.UUID_DEVICE_INFORMATION_FIRMWARE_VERSION_CHARACTERISTIC);
if (deviceFirmwareVersionChar == null ||
!mBluetoothGattClient.readCharacteristic(deviceFirmwareVersionChar)) {
Log.e(TAG, "readDeviceInfo: Failed to get device firmware revision characteristic");
return false;
}
return true;
}
/* Enables device firmware update notification of the connected keyboard. */
private boolean enableDfuNotification() {
Log.d(TAG, "enableDfuNotification");
if (mDfuChar == null) {
Log.w(TAG, "enableDfuNotification: DFU control point characteristic not initiated");
return false;
}
BluetoothGattDescriptor dfuDesc = mDfuChar.getDescriptor(
GattAttributeUUID.UUID_DFU_CONTROL_POINT_DESCRIPTOR);
if (dfuDesc == null ||
!dfuDesc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) ||
!mBluetoothGattClient.writeDescriptor(dfuDesc)) {
Log.e(TAG, "enableDfuNotification: Failed to init DFU descriptor write operation");
return false;
}
return true;
}
/* Switches the connected keyboard to DFU mode. */
private boolean enableDfuMode() {
Log.d(TAG, "enableDfuMode");
if (mDfuChar == null) {
Log.w(TAG, "enableDfuMode: DFU control point characteristic not initiated");
return false;
}
// Opcode: 0x01 -> Start DFU mode.
// 0x04 -> DFU type: application
final byte dfuOpcodeWithTypeApplication[] = new byte[]{0x01, 0x04};
if (!mDfuChar.setValue(dfuOpcodeWithTypeApplication) ||
!mBluetoothGattClient.writeCharacteristic(mDfuChar)) {
Log.e(TAG, "enableDfuMode: Failed to init DFU mode switch");
return false;
}
return true;
}
/* Checks if a Bluetooth Gatt operation is finished correctly. */
private boolean checkOperationStatus(int status) {
mGattOperationStatus = status;
if (mGattOperationStatus != BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "BluetoothGattCallback: GATT operation failure: " + mGattOperationStatus);
return false;
}
return true;
}
/* Starts DFU process. */
private void startDfuService(String keyboardName, String keyboardAddress) {
Log.d(TAG, "startDfuService");
changeDfuStatus(DFU_STATE_UPDATING);
String packageName = getApplicationContext().getPackageName();
int initResourceId = getResources().getIdentifier(
getString(R.string.target_firmware_init_file_name), "raw", packageName);
int imageResourceId = getResources().getIdentifier(
getString(R.string.target_firmware_image_file_name), "raw", packageName);
boolean keepBond = true;
Log.d(TAG, "Name: " + keyboardName + "\n" +
"Address: " + keyboardAddress + "\n" +
"Init file: " + getString(R.string.target_firmware_init_file_name) + "\n" +
"Image file: " + getString(R.string.target_firmware_image_file_name) + "\n" +
"Image type: Application(" + DfuService.TYPE_APPLICATION + ")\n" +
"Keep bond: " + keepBond);
final Intent service = new Intent(this, DfuService.class);
service.putExtra(DfuService.EXTRA_DEVICE_NAME, keyboardName);
service.putExtra(DfuService.EXTRA_DEVICE_ADDRESS, keyboardAddress);
service.putExtra(DfuService.EXTRA_INIT_FILE_RES_ID, initResourceId);
service.putExtra(DfuService.EXTRA_FILE_RES_ID, imageResourceId);
service.putExtra(DfuService.EXTRA_FILE_TYPE, DfuService.TYPE_APPLICATION);
service.putExtra(DfuService.EXTRA_KEEP_BOND, true);
startService(service);
}
/* Aborts DFU service if it is in progress. */
public void abortDfu() {
if (mDfuStatus != DFU_STATE_UPDATING) return;
final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_ABORT);
LocalBroadcastManager.getInstance(this).sendBroadcast(pauseAction);
// Wait 2 seconds for DFU abort request to finish.
try {
Thread.sleep(2000); // 2 seconds
Log.d(TAG, "abortDfu: Wait for DFU to abort");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/* State setter of GATT connection. */
private void changeGattState(int newStatus) {
mGattConnectionState = newStatus;
Log.i(TAG, "-- changeGattState: " + getGattStateString(mGattConnectionState));
}
/* Helper function for logging GATT state change. */
private String getGattStateString(int state) {
switch (state) {
case GATT_STATE_DISCONNECTED:
return "GATT_STATE_DISCONNECTED";
case GATT_STATE_CONNECTING:
return "GATT_STATE_CONNECTING";
case GATT_STATE_CONNECTED:
return "GATT_STATE_CONNECTED";
case GATT_STATE_DISCOVERING_SERVICES:
return "GATT_STATE_DISCOVERING_SERVICES";
case GATT_STATE_DISCONNECTING:
return "GATT_STATE_DISCONNECTING";
default:
return "Unknown state (" + state + ")";
}
}
/* State flow for the updater service. */
private void changeDfuStatus(int newStatus) {
switch (newStatus) {
case DFU_STATE_NOT_STARTED:
break;
case DFU_STATE_OBTAINING_INFO:
connectToKeyboard();
break;
case DFU_STATE_INFO_READY:
showUpdateNotification();
break;
case DFU_STATE_SWITCHING_TO_DFU_MODE:
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mDfuStatus == DFU_STATE_SWITCHING_TO_DFU_MODE)
changeDfuStatus(DFU_STATE_SWITCH_TO_DFU_MODE_ERROR);
}
}, SCAN_PERIOD * 2);
enableDfuNotification();
break;
case DFU_STATE_MODE_SWITCHED:
scanLeDevice(true);
break;
case DFU_STATE_UPDATING:
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mWakeLock.acquire();
break;
case DFU_STATE_UPDATE_COMPLETE:
mWakeLock.release();
terminateSelf();
break;
case DFU_STATE_INFO_NOT_SUITABLE:
terminateSelf();
break;
case DFU_STATE_OBTAIN_INFO_ERROR:
terminateSelf();
break;
case DFU_STATE_SWITCH_TO_DFU_MODE_ERROR:
terminateSelf();
break;
case DFU_STATE_UPDATE_ABORTED:
mWakeLock.release();
terminateSelf();
break;
default:
break;
}
mDfuStatus = newStatus;
Log.d(TAG, "---- changeDfuStatus: " + getDfuStateString(mDfuStatus));
}
/* Helper function for logging DFU state change. */
private String getDfuStateString(int state) {
switch (state) {
case DFU_STATE_NOT_STARTED:
return "DFU_STATE_NOT_STARTED";
case DFU_STATE_OBTAINING_INFO:
return "DFU_STATE_OBTAINING_INFO";
case DFU_STATE_INFO_READY:
return "DFU_STATE_INFO_READY";
case DFU_STATE_SWITCHING_TO_DFU_MODE:
return "DFU_STATE_SWITCHING_TO_DFU_MODE";
case DFU_STATE_MODE_SWITCHED:
return "DFU_STATE_MODE_SWITCHED";
case DFU_STATE_UPDATING:
return "DFU_STATE_UPDATING";
case DFU_STATE_UPDATE_COMPLETE:
return "DFU_STATE_UPDATE_COMPLETE";
case DFU_STATE_INFO_NOT_SUITABLE:
return "DFU_STATE_INFO_NOT_SUITABLE";
case DFU_STATE_OBTAIN_INFO_ERROR:
return "DFU_STATE_OBTAIN_INFO_ERROR";
case DFU_STATE_SWITCH_TO_DFU_MODE_ERROR:
return "DFU_STATE_SWITCH_TO_DFU_MODE_ERROR";
case DFU_STATE_UPDATE_ABORTED:
return "DFU_STATE_UPDATE_ABORTED";
default:
return "Unknown state (" + state + ")";
}
}
/* Handles GATT disconnection and DFU aborting before the service terminate itself. */
private void handleGattAndDfuCleanup() {
Log.d(TAG, "handleGattAndDfuCleanup");
if (mGattConnectionState == GATT_STATE_DISCONNECTED) return;
// Handle update process termination based on the current DFU state.
switch (mDfuStatus) {
case DFU_STATE_SWITCHING_TO_DFU_MODE:
scanLeDevice(false);
case DFU_STATE_OBTAINING_INFO:
case DFU_STATE_INFO_READY:
case DFU_STATE_INFO_NOT_SUITABLE:
case DFU_STATE_SWITCH_TO_DFU_MODE_ERROR:
disconnectFromKeyboard();
case DFU_STATE_NOT_STARTED:
case DFU_STATE_MODE_SWITCHED:
case DFU_STATE_UPDATE_COMPLETE:
case DFU_STATE_UPDATE_ABORTED:
break;
case DFU_STATE_UPDATING:
abortDfu();
dismissUpdateNotification();
break;
default:
break;
}
changeGattState(GATT_STATE_DISCONNECTED);
}
public class LocalBinder extends Binder {
KeyboardFirmwareUpdateService getService() {
return KeyboardFirmwareUpdateService.this;
}
}
}