blob: 575fe1916c5baa8f704ab565cf1c02dd7befd3c4 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package examples.baku.io.permissions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
import android.os.Binder;
import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.MaterialIcons;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import examples.baku.io.permissions.discovery.DeviceData;
import examples.baku.io.permissions.discovery.DevicePickerActivity;
import examples.baku.io.permissions.examples.ComposeActivity;
import examples.baku.io.permissions.examples.EmailActivity;
import examples.baku.io.permissions.messenger.Message;
import examples.baku.io.permissions.messenger.Messenger;
import examples.baku.io.permissions.util.Utils;
public class PermissionService extends Service {
private static final String TAG = PermissionService.class.getSimpleName();
static void l(String msg) {
Log.e(TAG, msg);
}
private static boolean mRunning;
public static boolean isRunning() {
return mRunning;
}
static final int FOREGROUND_NOTIFICATION_ID = -3278;
static final String KEY_BLESSINGS = PermissionManager.KEY_BLESSINGS;
public static final String EXTRA_COMMAND = "type";
public static final String EXTRA_REQUEST_ID = "requestId";
public static final String EXTRA_NOTIFICATION_ID = "notificationId";
public static final String EXTRA_ACTION_ID = "notificationId";
NotificationManager mNotificationManager;
FirebaseDatabase mFirebaseDB;
DatabaseReference mDevicesReference;
DatabaseReference mRequestsReference;
DatabaseReference mMessengerReference;
Messenger mMessenger;
DatabaseReference mPermissionsReference;
PermissionManager mPermissionManager;
Blessing mDeviceBlessing;
DatabaseReference mLocalDeviceReference;
private String mDeviceId;
private int mNotificationCounter = 0;
private int mActionCounter = 0;
final private Map<String, DeviceData> mDiscovered = new HashMap<>();
final private HashSet<String> mConstellation = new HashSet<>();
final private Map<String, Integer> mConstellationNotifications = new HashMap<>();
final private HashSet<DiscoveryListener> mDiscoveryListener = new HashSet<>();
final private Map<String, Integer> mDiscoveredNotifications = new HashMap<>();
final private Map<String, Integer> mRequestNotifications = new HashMap<>();
final private Map<String, ActionCallback> mActionListeners = new HashMap<>();
private Icon shareIcon;
private Icon zoomIcon;
private Icon closeIcon;
private Icon keyIcon;
private Icon grantIcon;
private Icon deviceIcon;
private Icon castIcon;
public interface ActionCallback {
boolean onAction(Intent intent); //return true if action is complete and the notification can be dismissed
}
public interface DiscoveryListener {
void onChange(Map<String, DeviceData> devices);
void onDisassociate(String deviceId);
}
public class PermissionServiceBinder extends Binder {
public PermissionService getInstance() {
return PermissionService.this;
}
}
public PermissionService() {
}
@Override
public void onCreate() {
super.onCreate();
mDeviceId = Settings.Secure.getString(getApplicationContext().getContentResolver(),
Settings.Secure.ANDROID_ID);
mFirebaseDB = FirebaseDatabase.getInstance();
mDevicesReference = mFirebaseDB.getReference("_devices");
mRequestsReference = mFirebaseDB.getReference("requests");
shareIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_share));
zoomIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_zoom_in));
closeIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_close));
keyIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_vpn_key));
grantIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_check));
deviceIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_phone_android));
castIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_cast));
mPermissionManager = new PermissionManager(mFirebaseDB.getReference(), mDeviceId);
mPermissionManager.getRootBlessing().setPermissions("documents/" + mDeviceId, PermissionManager.FLAG_ROOT);
mPermissionManager.join("public");
mPermissionManager.addOnRequestListener("*", new PermissionManager.OnRequestListener() {
@Override
public boolean onRequest(PermissionRequest request, Blessing blessing) {
int nId = mNotificationCounter++;
String sourceName = "unknown device";
String source = request.getSource();
if (source != null && mDiscovered.containsKey(source)) {
sourceName = mDiscovered.get(source).getName();
}
Intent acceptRequestIntent = new Intent(PermissionService.this, PermissionService.class);
acceptRequestIntent.putExtra(EXTRA_COMMAND, "acceptRequest");
acceptRequestIntent.putExtra(EXTRA_REQUEST_ID, request.getId());
acceptRequestIntent.putExtra(EXTRA_NOTIFICATION_ID, nId);
PendingIntent acceptRequestPendingIntent = PendingIntent.getService(PermissionService.this, mActionCounter++, acceptRequestIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Intent rejectRequestIntent = new Intent(PermissionService.this, PermissionService.class);
rejectRequestIntent.putExtra(EXTRA_COMMAND, "rejectRequest");
rejectRequestIntent.putExtra(EXTRA_REQUEST_ID, request.getId());
rejectRequestIntent.putExtra(EXTRA_NOTIFICATION_ID, nId);
PendingIntent rejectRequestPendingIntent = PendingIntent.getService(PermissionService.this, mActionCounter++, rejectRequestIntent, PendingIntent.FLAG_CANCEL_CURRENT);
mNotificationManager.cancel(nId);
Notification notification = new Notification.Builder(PermissionService.this)
.setSmallIcon(keyIcon)
.setContentTitle("Permission requestDialog from " + sourceName)
.addAction(new Notification.Action.Builder(grantIcon, "Grant", acceptRequestPendingIntent).build())
// .addAction(new Notification.Action.Builder(R.drawable.ic_close_black_24dp, "Reject", rejectRequestPendingIntent).build())
.setDeleteIntent(rejectRequestPendingIntent)
.setVibrate(new long[]{100})
.setPriority(Notification.PRIORITY_MAX)
.build();
mNotificationManager.notify(nId, notification);
mRequestNotifications.put(request.getId(), nId);
return true;
}
@Override
public void onRequestRemoved(PermissionRequest request, Blessing blessing) {
if (mRequestNotifications.containsKey(request.getId())) {
mNotificationManager.cancel(mRequestNotifications.get(request.getId()));
}
}
});
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
initForegroundNotification();
registerDevice();
initMessenger();
initDiscovery();
mRunning = true;
}
public void requestDialog(String requestId, String title, String subtitle, ActionCallback accept, ActionCallback reject) {
Integer previousNotificationId = mRequestNotifications.get(requestId);
if (previousNotificationId != null) {
mNotificationManager.cancel(previousNotificationId);
}
Notification notification = new Notification.Builder(PermissionService.this)
.setSmallIcon(keyIcon)
.setContentTitle(title)
.setSubText(subtitle)
.addAction(createAction(grantIcon, "Accept", UUID.randomUUID().toString(), accept))
.setDeleteIntent(createNotificationCallback(UUID.randomUUID().toString(), reject))
.setVibrate(new long[]{100})
.setPriority(Notification.PRIORITY_MAX)
.build();
int nId = mNotificationCounter++;
mNotificationManager.notify(nId, notification);
mRequestNotifications.put(requestId, nId);
}
public Messenger getMessenger() {
return mMessenger;
}
public void updateConstellationDevice(String dId) {
if (!mDiscovered.containsKey(dId)) return;
DeviceData device = mDiscovered.get(dId);
String title = device.getName();
String subtitle = device.getId(); //default
if (device.getStatus() != null && device.getStatus().containsKey("description")) {
subtitle = device.getStatus().get("description");
}
int icon = R.drawable.ic_phone_android_black_24dp;
Intent dismissIntent = new Intent(this, PermissionService.class);
dismissIntent.putExtra("type", "dismiss");
dismissIntent.putExtra("deviceId", dId);
PendingIntent dismissPending = PendingIntent.getService(this, mActionCounter++, dismissIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification.Builder notificationBuilder = new Notification.Builder(this)
.setContentTitle(title)
.setContentText(subtitle)
.setSmallIcon(deviceIcon)
.setVibrate(new long[]{100})
.setPriority(Notification.PRIORITY_MAX)
.setDeleteIntent(dismissPending);
//add contextual actions
if (mLocalDevice != null && mLocalDevice.getStatus().containsKey(ComposeActivity.EXTRA_MESSAGE_PATH)) {
final String localPath = mLocalDevice.getStatus().get(ComposeActivity.EXTRA_MESSAGE_PATH);
final String focus = device.getId();
notificationBuilder.addAction(createAction(castIcon, "Cast Message", "castMessage", new ActionCallback() {
@Override
public boolean onAction(Intent intent) {
try {
JSONObject castArgs = new JSONObject();
castArgs.put("activity", ComposeActivity.class.getSimpleName());
castArgs.put(ComposeActivity.EXTRA_MESSAGE_PATH, localPath);
mMessenger.to(focus).emit("cast", castArgs.toString());
} catch (JSONException e) {
e.printStackTrace();
}
return false;
}
}));
}
//add contextual actions
if (device != null && device.getStatus().containsKey(ComposeActivity.EXTRA_MESSAGE_PATH)) {
String focusPath = device.getStatus().get(ComposeActivity.EXTRA_MESSAGE_PATH);
Intent emailIntent = new Intent(PermissionService.this, ComposeActivity.class);
emailIntent.putExtra(ComposeActivity.EXTRA_MESSAGE_PATH, focusPath);
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
notificationBuilder.addAction(new Notification.Action.Builder(castIcon, "Pull Message", PendingIntent.getActivity(this, 0, emailIntent, PendingIntent.FLAG_CANCEL_CURRENT)).build());
}
mConstellation.add(dId);
Integer notificationId = mConstellationNotifications.get(dId);
if (notificationId == null) {
notificationId = mNotificationCounter++;
mConstellationNotifications.put(dId, notificationId);
}
Notification notification = notificationBuilder.build();
mNotificationManager.notify(notificationId, notification);
}
private Notification.Action createAction(Icon icon, String title, String actionId, ActionCallback callback) {
PendingIntent actionPendingIntent = createNotificationCallback(actionId, callback);
return new Notification.Action.Builder(icon, title, actionPendingIntent).build();
}
private PendingIntent createNotificationCallback(String actionId, ActionCallback callback) {
mActionListeners.put(actionId, callback);
Intent actionIntent = new Intent(this, PermissionService.class);
actionIntent.putExtra(EXTRA_COMMAND, "actionCallback");
actionIntent.putExtra(EXTRA_ACTION_ID, actionId);
return PendingIntent.getService(this, mActionCounter++, actionIntent, PendingIntent.FLAG_CANCEL_CURRENT);
}
public PermissionManager getPermissionManager() {
return mPermissionManager;
}
public String getDeviceId() {
return mDeviceId;
}
public Map<String, DeviceData> getDiscovered() {
return mDiscovered;
}
public void setStatus(String key, String value) {
mDevicesReference.child(mDeviceId).child("status").child(key).setValue(value);
}
public void clearStatus(String key) {
mDevicesReference.child(mDeviceId).child("status").child(key).removeValue();
}
public FirebaseDatabase getFirebaseDB() {
return mFirebaseDB;
}
@Override
public IBinder onBind(Intent intent) {
return new PermissionServiceBinder();
}
void initForegroundNotification() {
Intent contentIntent = new Intent(this, PermissionService.class);
PendingIntent contentPendingIntent = PendingIntent.getActivity(this, mActionCounter++, contentIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Intent discoverIntent = new Intent(getApplicationContext(), PermissionService.class);
discoverIntent.putExtra(EXTRA_COMMAND, "discover");
PendingIntent discoverPendingIntent = PendingIntent.getService(this, mActionCounter++, discoverIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent closeIntent = new Intent(getApplicationContext(), PermissionService.class);
closeIntent.putExtra(EXTRA_COMMAND, "close");
PendingIntent closePendingIntent = PendingIntent.getService(this, mActionCounter++, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification.Builder(this)
.setContentIntent(contentPendingIntent)
.setSmallIcon(shareIcon)
.setContentTitle("Discovery service running")
.addAction(new Notification.Action.Builder(zoomIcon, "Discover", discoverPendingIntent).build())
.addAction(new Notification.Action.Builder(closeIcon, "Stop", closePendingIntent).build())
.build();
startForeground(FOREGROUND_NOTIFICATION_ID, notification);
}
void refreshForegroundNotification(Notification notification) {
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
}
public Blessing getRootBlessing() {
return mPermissionManager.getRootBlessing();
}
public void initMessenger() {
mMessengerReference = mFirebaseDB.getReference("messages");
mMessenger = new Messenger(mDeviceId, mMessengerReference);
mMessenger.on("disassociate", new Messenger.Listener() {
@Override
public void call(Message msg, Messenger.Ack callback) {
}
});
mMessenger.on("cast", new Messenger.Listener() {
@Override
public void call(Message msg, Messenger.Ack callback) {
String args = msg.getMessage();
if (args != null) {
try {
JSONObject jsonArgs = new JSONObject(args);
if (jsonArgs.has("activity")) {
if (ComposeActivity.class.getSimpleName().equals(jsonArgs.getString("activity"))) {
String path = jsonArgs.getString(ComposeActivity.EXTRA_MESSAGE_PATH);
Intent emailIntent = new Intent(PermissionService.this, ComposeActivity.class);
emailIntent.putExtra(ComposeActivity.EXTRA_MESSAGE_PATH, path);
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(emailIntent);
}
}
//add cast source to constellation
updateConstellationDevice(msg.getSource());
} catch (JSONException e) {
e.printStackTrace();
}
}
}
});
}
public void removeFromConstellation(String deviceId) {
mConstellation.remove(deviceId);
mConstellationNotifications.remove(deviceId);
//revoke all blessings
for (Blessing blessing : mPermissionManager.getReceivedBlessings()) {
Blessing granted = blessing.getBlessing(deviceId);
if (granted != null) {
granted.revoke();
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//TODO: move to a broadcast receiver. StartService intents are not ideal.
if (intent != null && intent.hasExtra(EXTRA_COMMAND)) {
String type = intent.getStringExtra(EXTRA_COMMAND);
l("start command " + type);
if ("actionCallback".equals(type)) {
if (intent.hasExtra(EXTRA_ACTION_ID)) {
String aId = intent.getStringExtra(EXTRA_ACTION_ID);
if (mActionListeners.containsKey(aId)) {
boolean result = mActionListeners.get(aId).onAction(intent);
if (result) {
Integer nId = mRequestNotifications.get(aId);
if (nId != null) {
mNotificationManager.cancel(nId);
}
}
}
}
} else if ("discover".equals(type)) {
if (mDiscovered != null) {
Intent discoveryIntent = new Intent(this, DevicePickerActivity.class);
discoveryIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(discoveryIntent);
}
} else if ("acceptRequest".equals(type)) {
if (intent.hasExtra(EXTRA_REQUEST_ID)) {
String rId = intent.getStringExtra(EXTRA_REQUEST_ID);
PermissionRequest request = mPermissionManager.getRequest(rId);
if (request != null) {
mPermissionManager.grantRequest(request);
} else {
Toast.makeText(getApplicationContext(), "Expired requestDialog", 0).show();
}
}
if (intent.hasExtra(EXTRA_NOTIFICATION_ID)) {
mNotificationManager.cancel(intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1));
}
} else if ("rejectRequest".equals(type)) {
if (intent.hasExtra(EXTRA_REQUEST_ID)) {
String rId = intent.getStringExtra(EXTRA_REQUEST_ID);
mPermissionManager.finishRequest(rId);
}
} else if ("dismiss".equals(type)) {
String dId = intent.getStringExtra("deviceId");
if (dId != null) {
removeFromConstellation(dId);
}
} else if ("close".equals(type)) {
stopSelf();
}
}
return super.onStartCommand(intent, flags, startId);
}
void registerDevice() {
mLocalDeviceReference = mDevicesReference.child(mDeviceId);
mLocalDeviceReference.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (!dataSnapshot.exists()) {
resetLocalDevice();
} else {
try {
mLocalDevice = dataSnapshot.getValue(DeviceData.class);
} catch (DatabaseException e) {
e.printStackTrace();
}
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
private DeviceData mLocalDevice;
void resetLocalDevice() {
final String deviceName = android.os.Build.MODEL;
mLocalDevice = new DeviceData(mDeviceId, deviceName);
mLocalDevice.setActive(true);
mLocalDeviceReference.setValue(mLocalDevice);
}
void initDiscovery() {
mDevicesReference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
updateDevice(dataSnapshot);
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
updateDevice(dataSnapshot);
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
public void addDiscoveryListener(DiscoveryListener listener) {
mDiscoveryListener.add(listener);
}
private void updateDevice(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
String key = dataSnapshot.getKey();
if (!mDeviceId.equals(key)) {
try {
DeviceData device = dataSnapshot.getValue(DeviceData.class);
if (device != null) {
mDiscovered.put(key, device);
for (DiscoveryListener listener : mDiscoveryListener) {
listener.onChange(mDiscovered);
}
if (mConstellation.contains(key)) {
updateConstellationDevice(key);
}
}
} catch (DatabaseException e) {
e.printStackTrace();
}
}
}
}
@Override
public void onDestroy() {
//TODO: clean up firebase listeners in permission manager.
if (mPermissionManager != null) {
mPermissionManager.onDestroy();
}
super.onDestroy();
}
public static void start(Context context) {
context.startService(new Intent(context, PermissionService.class));
}
//convenience class for when context implements ServiceConnection
//throws cast exception
public static void bind(Context context) {
ServiceConnection connection = (ServiceConnection) context;
bind(context, connection);
}
public static void bind(Context context, ServiceConnection connection) {
context.bindService(new Intent(context, PermissionService.class), connection, BIND_AUTO_CREATE);
}
}