/*
* 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.collection.PwsClient;
import org.physical_web.collection.PwsResult;
import org.physical_web.collection.PwsResultCallback;
import org.physical_web.physicalweb.ble.AdvertiseDataUtils;
import android.annotation.TargetApi;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
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.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationManagerCompat;
import android.widget.Toast;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
/**
* Shares URLs via bluetooth.
* Also interfaces with PWS to shorten URLs that are too long for Eddystone URLs.
* Lastly, it surfaces a persistent notification whenever a URL is currently being broadcast.
**/
@TargetApi(21)
public class PhysicalWebBroadcastService extends Service {
private static final String TAG = PhysicalWebBroadcastService.class.getSimpleName();
private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
private static final int BROADCASTING_NOTIFICATION_ID = 6;
public static final String DISPLAY_URL_KEY = "displayUrl";
public static final String PREVIOUS_BROADCAST_URL_KEY = "previousUrl";
public static final int MAX_URI_LENGTH = 18;
private NotificationManagerCompat mNotificationManager;
private Handler mHandler = new Handler();
private String mDisplayUrl;
private boolean mStartedByRestart;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "in receiver");
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:
Log.d(TAG, "stop because BT off");
stopSelf();
break;
default:
}
}
}
};
/////////////////////////////////
// callbacks
/////////////////////////////////
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "SERVICE onCreate");
BluetoothManager bluetoothManager = (BluetoothManager) getApplicationContext()
.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothLeAdvertiser = bluetoothManager.getAdapter().getBluetoothLeAdvertiser();
mNotificationManager = NotificationManagerCompat.from(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
fetchBroadcastData(intent);
if (mDisplayUrl == null) {
stopSelf();
return START_STICKY;
}
Log.d(TAG, "SERVICE onStartCommand");
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
handleUrl();
return START_STICKY;
}
private void handleUrl() {
PwsClient pwsClient = new PwsClient();
pwsClient.resolve(Arrays.asList(mDisplayUrl), new PwsResultCallback() {
@Override
public void onPwsResult(PwsResult pwsResult) {
String fullUrl = pwsResult.getSiteUrl();
byte[] encodedUrl = AdvertiseDataUtils.encodeUri(fullUrl);
if(checkNeedsShortening(encodedUrl, fullUrl)) {
shortenAndBroadcastUrl(fullUrl);
} else {
broadcastUrl(encodedUrl);
}
}
@Override
public void onPwsResultAbsent(String url) {
toastError(R.string.invalid_url_error);
stopSelf();
}
@Override
public void onPwsResultError(Collection<String> urls, int httpResponseCode, Exception e) {
toastError(R.string.invalid_url_error);
stopSelf();
}
@Override
public void onResponseReceived(long durationMillis) {
}
});
}
private void shortenAndBroadcastUrl(String fullUrl) {
UrlShortenerClient.ShortenUrlCallback urlSetter =
new UrlShortenerClient.ShortenUrlCallback() {
@Override
public void onUrlShortened(String newUrl) {
broadcastUrl(AdvertiseDataUtils.encodeUri(newUrl));
}
@Override
public void onError(String oldUrl) {
toastError(R.string.shorten_error);
stopSelf();
}
};
UrlShortenerClient shortenerClient = UrlShortenerClient.getInstance(this);
shortenerClient.shortenUrl(fullUrl, urlSetter, TAG);
}
private void fetchBroadcastData(Intent intent) {
mStartedByRestart = intent == null;
if (intent == null) {
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mDisplayUrl = sharedPrefs.getString(PREVIOUS_BROADCAST_URL_KEY, null);
return;
}
mDisplayUrl = intent.getStringExtra(DISPLAY_URL_KEY);
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString(PREVIOUS_BROADCAST_URL_KEY, mDisplayUrl)
.commit();
}
private boolean checkNeedsShortening(byte[] encodedUrl, String fullUrl) {
return !hasValidUrlLength(encodedUrl.length) || !checkAndHandleAsciiUrl(fullUrl);
}
private static boolean hasValidUrlLength(int uriLength) {
return 0 < uriLength && uriLength <= MAX_URI_LENGTH;
}
private boolean checkAndHandleAsciiUrl(String url) {
boolean isCompliant = false;
try {
URI uri = new URI(url);
String urlString = uri.toASCIIString();
isCompliant = url.equals(urlString);
} catch (URISyntaxException e) {
toastError(R.string.no_url_error);
}
return isCompliant;
}
@Override
public void onDestroy() {
Log.d(TAG, "SERVICE onDestroy");
unregisterReceiver(stopServiceReceiver);
unregisterReceiver(mReceiver);
disableUrlBroadcasting();
super.onDestroy();
}
@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) {
Log.d(TAG, "URL is broadcasting");
Utils.createBroadcastNotification(PhysicalWebBroadcastService.this, stopServiceReceiver,
BROADCASTING_NOTIFICATION_ID, getString(R.string.broadcast_notif), mDisplayUrl,
"myFilter");
if (!mStartedByRestart) {
Toast.makeText(getApplicationContext(), getString(R.string.url_broadcast),
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[] url) {
final AdvertiseData advertisementData = AdvertiseDataUtils.getAdvertisementData(url);
final AdvertiseSettings advertiseSettings = AdvertiseDataUtils.getAdvertiseSettings(false);
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
mBluetoothLeAdvertiser.startAdvertising(advertiseSettings,
advertisementData, mAdvertiseCallback);
}
// Turn off URL broadcasting
public void disableUrlBroadcasting() {
Log.d(TAG, "disableUrlBroadcasting");
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
mNotificationManager.cancel(BROADCASTING_NOTIFICATION_ID);
}
protected BroadcastReceiver stopServiceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, context.toString());
stopSelf();
}
};
private void toastError(int messageId) {
Toast.makeText(getApplicationContext(), getString(messageId), Toast.LENGTH_LONG).show();
}
}