package com.codegy.aerlink.services.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.*;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
import android.util.Log;
import com.codegy.aerlink.Constants;
import com.codegy.aerlink.R;
import com.codegy.aerlink.connection.command.Command;
import com.codegy.aerlink.services.aerlink.ALSConstants;
import com.codegy.aerlink.utils.ScheduledTask;
import com.codegy.aerlink.utils.ServiceHandler;
import com.codegy.aerlink.utils.ServiceUtils;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* Created by Guiye on 18/5/15.
*/
public class NotificationServiceHandler extends ServiceHandler {
private static final String LOG_TAG = NotificationServiceHandler.class.getSimpleName();
public static final int NOTIFICATION_REGULAR = 1000;
private static final long VIBRATION_PATTERN[] = { 100, 400, 200, 40, 40, 40, 70, 200 };
private static final long SILENT_VIBRATION_PATTERN[] = { 200, 110 };
private Context mContext;
private ServiceUtils mServiceUtils;
private NotificationPacketProcessor mPacketProcessor;
private int mNotificationNumber = 0;
private List<NotificationData> mPendingNotifications = new ArrayList<>();
public NotificationServiceHandler(Context context, ServiceUtils serviceUtils) {
this.mContext = context;
this.mServiceUtils = serviceUtils;
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Constants.IA_POSITIVE);
intentFilter.addAction(Constants.IA_NEGATIVE);
intentFilter.addAction(Constants.IA_DELETE);
context.registerReceiver(mBroadcastReceiver, intentFilter);
}
@Override
public void close() {
mNotificationNumber = 0;
reset();
mContext.unregisterReceiver(mBroadcastReceiver);
}
@Override
public void reset() {
mPacketProcessor = null;
mPendingNotifications.clear();
cancelClearOldNotificationsTask();
}
@Override
public UUID getServiceUUID() {
return ANCSConstants.SERVICE_UUID;
}
@Override
public List<String> getCharacteristicsToSubscribe() {
List<String> characteristics = new ArrayList<>();
characteristics.add(ANCSConstants.CHARACTERISTIC_DATA_SOURCE);
characteristics.add(ANCSConstants.CHARACTERISTIC_NOTIFICATION_SOURCE);
return characteristics;
}
@Override
public boolean canHandleCharacteristic(BluetoothGattCharacteristic characteristic) {
String characteristicUUID = characteristic.getUuid().toString().toLowerCase();
return characteristicUUID.equals(ANCSConstants.CHARACTERISTIC_DATA_SOURCE) || characteristicUUID.equals(ANCSConstants.CHARACTERISTIC_NOTIFICATION_SOURCE);
}
@Override
public void handleCharacteristic(BluetoothGattCharacteristic characteristic) {
// Get notification packet from iOS
byte[] packet = characteristic.getValue();
switch (characteristic.getUuid().toString().toLowerCase()) {
case ANCSConstants.CHARACTERISTIC_DATA_SOURCE:
if (mPacketProcessor == null && packet.length >= 5) {
byte[] notificationUID = new byte[] { packet[1], packet[2], packet[3], packet[4] };
int notificationIndex = -1;
for (int i = 0; i < mPendingNotifications.size(); i++) {
NotificationData notificationData = mPendingNotifications.get(i);
if (notificationData.compareUID(notificationUID)) {
notificationIndex = i;
break;
}
}
if (notificationIndex != -1) {
mPacketProcessor = new NotificationPacketProcessor(mPendingNotifications.get(notificationIndex));
mPendingNotifications.remove(notificationIndex);
}
}
if (mPacketProcessor != null) {
// Only remove callback if we are getting useful data
cancelClearOldNotificationsTask();
mPacketProcessor.process(packet);
if (mPacketProcessor.hasFinishedProcessing()) {
NotificationData notificationData = mPacketProcessor.getNotificationData();
if (notificationData != null) {
if (notificationData.isIncomingCall()) {
onIncomingCall(notificationData);
}
else {
onNotificationReceived(notificationData);
}
}
mPacketProcessor = null;
}
}
if (mPendingNotifications.size() > 0 || mPacketProcessor != null) {
// Clear notifications in case data never arrives
scheduleClearOldNotificationsTask();
}
break;
case ANCSConstants.CHARACTERISTIC_NOTIFICATION_SOURCE:
try {
switch (packet[0]) {
case ANCSConstants.EventIDNotificationAdded:
case ANCSConstants.EventIDNotificationModified:
// Request attributes for the new notification
byte[] getAttributesPacket = new byte[] {
ANCSConstants.CommandIDGetNotificationAttributes,
// UID
packet[4], packet[5], packet[6], packet[7],
// App Identifier - NotificationAttributeIDAppIdentifier
ANCSConstants.NotificationAttributeIDAppIdentifier,
// Title - NotificationAttributeIDTitle
// Followed by a 2-bytes max length parameter
ANCSConstants.NotificationAttributeIDTitle,
(byte) 0xff,
(byte) 0xff,
// Message - NotificationAttributeIDMessage
// Followed by a 2-bytes max length parameter
ANCSConstants.NotificationAttributeIDMessage,
(byte) 0xff,
(byte) 0xff,
};
NotificationData notificationData = new NotificationData(packet);
mPendingNotifications.add(notificationData);
if (notificationData.hasPositiveAction()) {
getAttributesPacket = NotificationPacketProcessor.concat(getAttributesPacket, new byte[]{
// Positive Action Label - NotificationAttributeIDPositiveActionLabel
ANCSConstants.NotificationAttributeIDPositiveActionLabel
});
}
if (notificationData.hasNegativeAction()) {
getAttributesPacket = NotificationPacketProcessor.concat(getAttributesPacket, new byte[]{
// Negative Action Label - NotificationAttributeIDNegativeActionLabel
ANCSConstants.NotificationAttributeIDNegativeActionLabel
});
}
Command getAttributesCommand = new Command(ANCSConstants.SERVICE_UUID, ANCSConstants.CHARACTERISTIC_CONTROL_POINT, getAttributesPacket);
mServiceUtils.addCommandToQueue(getAttributesCommand);
// Clear notifications in case data never arrives
scheduleClearOldNotificationsTask();
break;
case ANCSConstants.EventIDNotificationRemoved:
if (packet[2] == 1) {
// Call ended
onCallEnded();
}
else {
// Cancel notification in watch
String notificationId = new String(Arrays.copyOfRange(packet, 4, 8));
onNotificationCanceled(notificationId);
}
break;
}
}
catch(Exception e) {
Log.d(LOG_TAG, "error");
e.printStackTrace();
}
break;
}
}
private void onIncomingCall(NotificationData notificationData) {
Log.d(LOG_TAG, "Incoming call");
try {
Intent phoneIntent = new Intent(mContext, PhoneActivity.class);
phoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
phoneIntent.putExtra(Constants.IE_NOTIFICATION_UID, notificationData.getUID());
phoneIntent.putExtra(Constants.IE_NOTIFICATION_TITLE, notificationData.getTitle());
phoneIntent.putExtra(Constants.IE_NOTIFICATION_MESSAGE, notificationData.getMessage());
mContext.startActivity(phoneIntent);
}
catch (Exception e) {
e.printStackTrace();
}
}
private void onNotificationReceived(NotificationData notificationData) {
Log.d(LOG_TAG, "Notification received");
// Build pending intent for when the user swipes the card away
Intent deleteIntent = new Intent(Constants.IA_DELETE);
deleteIntent.putExtra(Constants.IE_NOTIFICATION_UID, notificationData.getUID());
PendingIntent deleteAction = PendingIntent.getBroadcast(mContext, mNotificationNumber, deleteIntent, 0);
Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setContentTitle(notificationData.getTitle())
.setContentText(notificationData.getMessage())
.setSmallIcon(notificationData.getAppIcon())
.setGroup(notificationData.getAppId())
.setDeleteIntent(deleteAction)
.setPriority(Notification.PRIORITY_MAX);
if (notificationData.isUnknown() && !notificationData.getAppId().isEmpty()) {
Bitmap bitmap = loadImageFromStorage(notificationData.getAppId());
if (bitmap != null) {
Log.i(LOG_TAG, "Icon loaded");
Icon icon = Icon.createWithBitmap(bitmap);
notificationBuilder.setSmallIcon(icon);
}
else {
Bitmap background = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.bg_notification);
Notification.WearableExtender wearableExtender = new Notification.WearableExtender()
.setBackground(background);
notificationBuilder.extend(wearableExtender);
Log.i(LOG_TAG, "Requesting icon");
String dataString = (char) 0x01 + notificationData.getAppId();
Command iconCommand = new Command(ALSConstants.SERVICE_UUID, ALSConstants.CHARACTERISTIC_UTILS_ACTION, dataString.getBytes());
mServiceUtils.addCommandToQueue(iconCommand);
}
}
else {
Bitmap background;
if (notificationData.getBackground() != -1) {
background = BitmapFactory.decodeResource(mContext.getResources(), notificationData.getBackground());
}
else {
background = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
background.eraseColor(notificationData.getBackgroundColor());
}
Notification.WearableExtender wearableExtender = new Notification.WearableExtender()
.setBackground(background);
notificationBuilder.extend(wearableExtender);
}
// Build positive action intent only if available
if (notificationData.getPositiveAction() != null) {
Intent positiveIntent = new Intent(Constants.IA_POSITIVE);
positiveIntent.putExtra(Constants.IE_NOTIFICATION_UID, notificationData.getUID());
PendingIntent positiveActionIntent = PendingIntent.getBroadcast(mContext, mNotificationNumber, positiveIntent, 0);
Icon icon = Icon.createWithResource(mContext, R.drawable.ic_action_accept);
Notification.Action positiveAction = new Notification.Action.Builder(icon, notificationData.getPositiveAction(), positiveActionIntent).build();
notificationBuilder.addAction(positiveAction);
}
// Build negative action intent only if available
if (notificationData.getNegativeAction() != null) {
Intent negativeIntent = new Intent(Constants.IA_NEGATIVE);
negativeIntent.putExtra(Constants.IE_NOTIFICATION_UID, notificationData.getUID());
PendingIntent negativeActionIntent = PendingIntent.getBroadcast(mContext, mNotificationNumber, negativeIntent, 0);
Icon icon = Icon.createWithResource(mContext, R.drawable.ic_action_remove);
Notification.Action negativeAction = new Notification.Action.Builder(icon, notificationData.getNegativeAction(), negativeActionIntent).build();
notificationBuilder.addAction(negativeAction);
}
if (!notificationData.isPreExisting()) {
if (!notificationData.isSilent()) {
notificationBuilder.setVibrate(VIBRATION_PATTERN);
}
else {
notificationBuilder.setVibrate(SILENT_VIBRATION_PATTERN);
}
}
// Build and notify
Notification notification = notificationBuilder.build();
mServiceUtils.notify(notificationData.getUIDString(), NOTIFICATION_REGULAR, notification);
mNotificationNumber++;
}
private void onCallEnded() {
Log.d(LOG_TAG, "Call ended");
mContext.sendBroadcast(new Intent(Constants.IA_END_CALL));
}
private void onNotificationCanceled(String notificationId) {
Log.d(LOG_TAG, "Notification canceled");
mServiceUtils.cancelNotification(notificationId, NOTIFICATION_REGULAR);
}
private ScheduledTask mClearOldNotificationsTask;
private void scheduleClearOldNotificationsTask() {
if (mClearOldNotificationsTask == null) {
mClearOldNotificationsTask = new ScheduledTask(1000, mContext.getMainLooper(), new Runnable() {
@Override
public void run() {
Log.i(LOG_TAG, "Clear old notifications");
mPacketProcessor = null;
mPendingNotifications.clear();
}
});
}
else {
mClearOldNotificationsTask.cancel();
}
mClearOldNotificationsTask.schedule();
}
private void cancelClearOldNotificationsTask() {
if (mClearOldNotificationsTask != null) {
mClearOldNotificationsTask.cancel();
}
}
private Bitmap loadImageFromStorage(String bundleIdentifier) {
ContextWrapper cw = new ContextWrapper(mContext);
File directory = cw.getDir("AppIconDir", Context.MODE_PRIVATE);
File path = new File(directory, bundleIdentifier+".png");
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(new FileInputStream(path));
}
catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// perform notification action: immediately
// delete intent: after 7~8sec.
try {
String action = intent.getAction();
byte[] UID = intent.getByteArrayExtra(Constants.IE_NOTIFICATION_UID);
String notificationId = new String(UID);
// Dismiss notification
mServiceUtils.cancelNotification(notificationId, NOTIFICATION_REGULAR);
byte actionId = ANCSConstants.ActionIDPositive;
if (action.equals(Constants.IA_NEGATIVE) | action.equals(Constants.IA_DELETE)) {
actionId = ANCSConstants.ActionIDNegative;
}
// Perform user selected action
byte[] performActionPacket = {
ANCSConstants.CommandIDPerformNotificationAction,
// Notification UID
UID[0], UID[1], UID[2], UID[3],
// Action Id
actionId
};
Command performActionCommand = new Command(ANCSConstants.SERVICE_UUID, ANCSConstants.CHARACTERISTIC_CONTROL_POINT, performActionPacket);
mServiceUtils.addCommandToQueue(performActionCommand);
}
catch (Exception e) {
Log.d(LOG_TAG, "error");
e.printStackTrace();
}
}
};
}