/*
* Copyright 2016 Google Inc. All rights reserved.
*
* 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 org.physical_web.physicalweb;
import org.physical_web.physicalweb.ble.AdvertiseDataUtils;
import android.annotation.TargetApi;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationManagerCompat;
import android.widget.Toast;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.UUID;
/**
* Shares a Web page via bluetooth.
* Lastly, it surfaces a persistent notification whenever a FatBeacon is currently being broadcast.
**/
@TargetApi(21)
public class FatBeaconBroadcastService extends Service {
private static final String TAG = FatBeaconBroadcastService.class.getSimpleName();
private static final String SERVICE_UUID = "ae5946d4-e587-4ba8-b6a5-a97cca6affd3";
private static final UUID CHARACTERISTIC_WEBPAGE_UUID = UUID.fromString(
"d1a517f0-2499-46ca-9ccc-809bc1c966fa");
private static final String PREVIOUS_BROADCAST_INFO_KEY = "previousInfo";
private static final int BROADCASTING_NOTIFICATION_ID = 8;
private boolean mStartedByRestart;
private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
private NotificationManagerCompat mNotificationManager;
private String mDisplayInfo;
private BluetoothManager mBluetoothManager;
private BluetoothGattServer mGattServer;
private byte[] data;
public static final String TITLE_KEY = "title";
public static final String URI_KEY = "uri";
/*
* Callback handles all incoming requests from GATT clients.
* From connections to read/write requests.
*/
private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
private int transferSpeed = 20;
private int queueOffset;
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
super.onConnectionStateChange(device, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "Connected to device " + device.getAddress());
queueOffset = 0;
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "Disconnected from device " + device.getAddress());
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device,
int requestId,
int offset,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
Log.i(TAG, "onCharacteristicReadRequest " + characteristic.getUuid().toString());
if (CHARACTERISTIC_WEBPAGE_UUID.equals(characteristic.getUuid())) {
Log.d(TAG, "Data length:" + data.length + ", offset:" + queueOffset);
if (queueOffset < data.length) {
int end = queueOffset + transferSpeed >= data.length ?
data.length : queueOffset + transferSpeed;
Log.d(TAG, "Data length:" + data.length + ", offset:" + queueOffset + ", end:" + end);
mGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_SUCCESS,
0,
Arrays.copyOfRange(data, queueOffset, end));
queueOffset = end;
} else if (queueOffset == data.length) {
mGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_SUCCESS,
0,
new byte[]{});
queueOffset++;
}
}
/*
* Unless the characteristic supports WRITE_NO_RESPONSE,
* always send a response back for any request.
*/
mGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
super.onMtuChanged(device, mtu);
transferSpeed = mtu - 5;
}
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
switch (state) {
case BluetoothAdapter.STATE_OFF:
stopSelf();
break;
default:
}
}
}
};
/////////////////////////////////
// callbacks
/////////////////////////////////
@Override
public void onCreate() {
super.onCreate();
mBluetoothLeAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
mNotificationManager = NotificationManagerCompat.from(this);
mBluetoothManager = (BluetoothManager) this.getSystemService(Context.BLUETOOTH_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
fetchBroadcastData(intent);
if (mDisplayInfo == null || data == null) {
stopSelf();
return START_STICKY;
}
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
broadcastUrl();
initGattServer();
return START_STICKY;
}
private void fetchBroadcastData(Intent intent) {
if ((mStartedByRestart = intent == null)) {
mDisplayInfo = PreferenceManager.getDefaultSharedPreferences(this)
.getString(PREVIOUS_BROADCAST_INFO_KEY, null);
return;
}
mDisplayInfo = intent.getStringExtra(TITLE_KEY);
String intentUri = intent.getStringExtra(URI_KEY);
if (intentUri == null) {
return;
}
try {
data = Utils.getBytes(getContentResolver().openInputStream(Uri.parse(intentUri)));
} catch (IOException e) {
data = null;
Log.e(TAG, "Error reading file");
}
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString(PREVIOUS_BROADCAST_INFO_KEY, mDisplayInfo)
.apply();
}
@Override
public void onDestroy() {
unregisterReceiver(stopServiceReceiver);
unregisterReceiver(mReceiver);
disableUrlBroadcasting();
super.onDestroy();
}
// Fires when user swipes away app from the recent apps list
@Override
public void onTaskRemoved (Intent rootIntent) {
stopSelf();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
// The callbacks for the ble advertisement events
private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
// Fires when the URL is successfully being advertised
@Override
public void onStartSuccess(AdvertiseSettings advertiseSettings) {
Utils.createBroadcastNotification(FatBeaconBroadcastService.this, stopServiceReceiver,
BROADCASTING_NOTIFICATION_ID, getString(R.string.fatbeacon_notification_title),
mDisplayInfo, "fatBeaconFilter");
if (!mStartedByRestart) {
Toast.makeText(getApplicationContext(), R.string.fatbeacon_broadcasting_confirmation,
Toast.LENGTH_LONG).show();
}
}
// Fires when the URL could not be advertised
@Override
public void onStartFailure(int result) {
Log.d(TAG, "onStartFailure" + result);
}
};
/////////////////////////////////
// utilities
/////////////////////////////////
// Broadcast via bluetooth the stored URL
private void broadcastUrl() {
byte[] bytes = null;
try {
bytes = mDisplayInfo.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Could not encode URL", e);
return;
}
AdvertiseData advertiseData = AdvertiseDataUtils.getFatBeaconAdvertisementData(bytes);
AdvertiseSettings advertiseSettings = AdvertiseDataUtils.getAdvertiseSettings(true);
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
mBluetoothLeAdvertiser.startAdvertising(advertiseSettings, advertiseData, mAdvertiseCallback);
}
// Turn off URL broadcasting
private void disableUrlBroadcasting() {
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
mNotificationManager.cancel(BROADCASTING_NOTIFICATION_ID);
}
private BroadcastReceiver stopServiceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
stopSelf();
}
};
private void initGattServer() {
mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
BluetoothGattService service = new BluetoothGattService(UUID.fromString(SERVICE_UUID),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
BluetoothGattCharacteristic webpage = new BluetoothGattCharacteristic(
CHARACTERISTIC_WEBPAGE_UUID, BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ);
service.addCharacteristic(webpage);
mGattServer.addService(service);
}
}