Added the /permissions directory. permissions contains an example android application that provides a backend for managing permissions in between devices in cross-device applications. The backend uses Firebase to communicate between different devices. example/EmailActivity uses this backend to cast email messages to other devices.
Change-Id: I19a390758629973851219c2fd8039f021ddc3462
diff --git a/permissions/.gitignore b/permissions/.gitignore
new file mode 100644
index 0000000..3c605de
--- /dev/null
+++ b/permissions/.gitignore
@@ -0,0 +1,19 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+
+# google-services / firebase configuration
+google-services.json
+
+# OSX files
+.DS_Store
+# Windows thumbnail db
+Thumbs.db
+
+#NDK
+obj/
\ No newline at end of file
diff --git a/permissions/app/proguard-rules.pro b/permissions/app/proguard-rules.pro
new file mode 100644
index 0000000..1dd87d6
--- /dev/null
+++ b/permissions/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/google/home/phamilton/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/Blessing.java b/permissions/app/src/main/java/examples/baku/io/permissions/Blessing.java
new file mode 100644
index 0000000..3a34af1
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/Blessing.java
@@ -0,0 +1,233 @@
+// 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 com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.ValueEventListener;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Stack;
+import java.util.UUID;
+
+/**
+ * Created by phamilton on 7/9/16.
+ */
+public class Blessing implements Iterable<Blessing.Rule> {
+
+ private static final String KEY_PERMISSIONS = "_permissions";
+ private static final String KEY_RULES = "rules";
+
+ private String id;
+ // private String pattern;
+ private String source;
+ private String target;
+ private DatabaseReference ref;
+ private DatabaseReference rulesRef;
+ private DataSnapshot snapshot;
+
+ final private Map<String, PermissionReference> refCache = new HashMap<>();
+
+ public Blessing(DataSnapshot snapshot) {
+ setSnapshot(snapshot);
+ this.id = snapshot.child("id").getValue(String.class);
+ this.target = snapshot.child("target").getValue(String.class);
+ if (snapshot.hasChild("source"))
+ this.source = snapshot.child("source").getValue(String.class);
+ }
+
+ public Blessing(String target, String source, DatabaseReference ref) {
+ setRef(ref);
+ setId(ref.getKey());
+ setSource(source);
+ setTarget(target);
+ ref.addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ setSnapshot(dataSnapshot);
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+ databaseError.toException().printStackTrace();
+ }
+ });
+ }
+
+ public boolean isSynched() {
+ return snapshot != null;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ ref.child("id").setValue(id);
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ ref.child("source").setValue(source);
+ }
+
+ public void setTarget(String target) {
+ this.target = target;
+ ref.child("target").setValue(target);
+ }
+
+ public void setSnapshot(DataSnapshot snapshot) {
+ if (!snapshot.exists()) {
+ throw new IllegalArgumentException("empty snapshot");
+ }
+ this.snapshot = snapshot;
+ setRef(snapshot.getRef());
+ }
+
+ public Blessing setPermissions(String path, int permissions) {
+ getRef(path).setPermission(permissions);
+ return this;
+ }
+
+ public Blessing clearPermissions(String path) {
+ getRef(path).clearPermission();
+ return this;
+ }
+
+ //delete all permission above path
+ public Blessing revoke(String path) {
+ if (path != null) {
+ rulesRef.child(path).removeValue();
+ } else {
+ rulesRef.removeValue();
+ }
+ return this;
+ }
+
+ public PermissionReference getRef(String path) {
+ PermissionReference result = refCache.get(path);
+ if (result == null) {
+ result = new PermissionReference(rulesRef, path);
+ refCache.put(path, result);
+ }
+ return result;
+ }
+
+ public void setRef(DatabaseReference ref) {
+ this.ref = ref;
+ this.rulesRef = ref.child(KEY_RULES);
+ }
+
+ public int getPermissionAt(String path, int starting) {
+ if (!isSynched()) { //snapshot not retrieved
+ return starting;
+ }
+ if (path == null) {
+ throw new IllegalArgumentException("illegal path value");
+ }
+ String[] pathItems = path.split("/");
+ DataSnapshot currentNode = snapshot;
+ if (currentNode.hasChild(KEY_PERMISSIONS)) {
+ starting |= currentNode.child(KEY_PERMISSIONS).getValue(Integer.class);
+ }
+ for (int i = 0; i < pathItems.length; i++) {
+ if (currentNode.hasChild(pathItems[i])) {
+ currentNode = snapshot.child(pathItems[i]);
+ } else { //child doesn't exist
+ break;
+ }
+ if (currentNode.hasChild(KEY_PERMISSIONS)) {
+ starting |= currentNode.child(KEY_PERMISSIONS).getValue(Integer.class);
+ }
+ }
+ return starting;
+ }
+
+ @Override
+ public Iterator<Rule> iterator() {
+ if (!isSynched()) {
+ return null;
+ }
+ final Stack<DataSnapshot> nodeStack = new Stack<>();
+ nodeStack.push(snapshot.child(KEY_RULES));
+
+ final Stack<Rule> inheritanceStack = new Stack<>();
+ inheritanceStack.push(new Rule(null, 0)); //default rule
+
+ return new Iterator<Rule>() {
+ @Override
+ public boolean hasNext() {
+ return !nodeStack.isEmpty();
+ }
+
+ @Override
+ public Rule next() {
+ DataSnapshot node = nodeStack.pop();
+ Rule inheritedRule = inheritanceStack.pop();
+
+ Rule result = new Rule();
+ String key = node.getKey();
+ if (!KEY_RULES.equals(key)) { //key_rules is the root directory
+ if (inheritedRule.path != null) {
+ result.path = inheritedRule.path + "/" + key;
+ } else {
+ result.path = key;
+ }
+ }
+
+ result.permissions = inheritedRule.permissions;
+ if (node.hasChild(KEY_PERMISSIONS)) {
+ result.permissions |= node.child(KEY_PERMISSIONS).getValue(Integer.class);
+ }
+ for (final DataSnapshot child : node.getChildren()) {
+ if (child.getKey().startsWith("_")) { //ignore keys with '_' prefix
+ continue;
+ }
+ nodeStack.push(child);
+ inheritanceStack.push(result);
+ }
+ return result;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public static class Rule {
+ private String path;
+ private int permissions;
+
+ public Rule() {
+ }
+
+ public Rule(String path, int permissions) {
+ this.path = path;
+ this.permissions = permissions;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public int getPermissions() {
+ return permissions;
+ }
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionManager.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionManager.java
new file mode 100644
index 0000000..851ae81
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionManager.java
@@ -0,0 +1,374 @@
+// 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 com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.firebase.database.ChildEventListener;
+import com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.ValueEventListener;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+
+/**
+ * Created by phamilton on 6/28/16.
+ */
+public class PermissionManager {
+
+ DatabaseReference mDatabaseRef;
+ DatabaseReference mBlessingsRef;
+ DatabaseReference mRequestsRef;
+
+ public static final int FLAG_DEFAULT = 0;
+ public static final int FLAG_WRITE = 1 << 0;
+ public static final int FLAG_READ = 1 << 1;
+ public static final int FLAG_PUSH = 1 << 2; //2-way
+// public static final int FLAG_REFER = 1 << 3; //1-way
+
+ static final String KEY_PERMISSIONS = "_permissions";
+ static final String KEY_REQUESTS = "_requests";
+ static final String KEY_BLESSINGS = "_blessings";
+
+ private String mId;
+
+ final Map<String, PermissionRequest> mRequests = new HashMap<>();
+
+ // final Map<String, Set<OnRequestListener>> requestListeners = new HashMap<>();
+ final Set<OnRequestListener> requestListeners = new HashSet<>();
+ final Multimap<String, OnReferralListener> referralListeners = HashMultimap.create();
+
+ final Map<String, Blessing> mBlessings = new HashMap<>();
+ //<targetId, blessingId>
+ //TODO: allow for multiple granted blessings per target
+ final Map<String, Blessing> mGrantedBlessings = new HashMap<>();
+
+ final Map<String, Integer> mCachedPermissions = new HashMap<>();
+ final Multimap<String, OnPermissionChangeListener> mPermissionValueEventListeners = HashMultimap.create();
+ final Multimap<String, String> mNearestAncestors = HashMultimap.create();
+
+
+ //TODO: replace string ownerId with Auth
+ public PermissionManager(final DatabaseReference databaseReference, String owner) {
+ this.mDatabaseRef = databaseReference;
+ this.mId = owner;
+
+ mRequestsRef = databaseReference.child(KEY_REQUESTS);
+ //TODO: only consider requests from sources within the constelattion
+ mRequestsRef.addChildEventListener(requestListener);
+
+ mBlessingsRef = mDatabaseRef.child(KEY_BLESSINGS);
+ mBlessingsRef.orderByChild("target").equalTo(mId).addChildEventListener(blessingListener);
+ mBlessingsRef.orderByChild("source").equalTo(mId).addListenerForSingleValueEvent(grantedBlessingListener);
+ }
+
+ void onBlessingUpdated(DataSnapshot snapshot) {
+ if (!snapshot.exists()) {
+ throw new IllegalArgumentException("snapshot value doesn't exist");
+ }
+ String key = snapshot.getKey();
+ Blessing blessing = mBlessings.get(key);
+ if (blessing == null) {
+ blessing = new Blessing(snapshot);
+ mBlessings.put(key, blessing);
+ } else {
+ blessing.setSnapshot(snapshot);
+ }
+
+ refreshPermissions();
+ }
+
+ //TODO: optimize this mess. Currently, recalculating entire permission tree.
+ void refreshPermissions() {
+ Map<String, Integer> updatedPermissions = new HashMap<>();
+ for (Blessing blessing : mBlessings.values()) {
+ if (blessing.isSynched()) {
+ for (Blessing.Rule rule : blessing) {
+ String path = rule.getPath();
+ if (updatedPermissions.containsKey(path)) {
+ updatedPermissions.put(path, updatedPermissions.get(path) | rule.getPermissions());
+ } else {
+ updatedPermissions.put(path, rule.getPermissions());
+ }
+ }
+ }
+ }
+
+ mNearestAncestors.clear();
+ for (String path : mPermissionValueEventListeners.keySet()) {
+ String nearestAncestor = getNearestCommonAncestor(path, updatedPermissions.keySet());
+ if (nearestAncestor != null) {
+ mNearestAncestors.put(nearestAncestor, path);
+ }
+ }
+
+ Set<String> changedPermissions = new HashSet<>();
+
+ Set<String> removedPermissions = new HashSet<>(mCachedPermissions.keySet());
+ removedPermissions.removeAll(updatedPermissions.keySet());
+ for (String path : removedPermissions) {
+ mCachedPermissions.remove(path);
+ String newPath = getNearestCommonAncestor(path, updatedPermissions.keySet());
+ changedPermissions.add(newPath); //reset to default
+ }
+
+ for (String path : updatedPermissions.keySet()) {
+ int current = updatedPermissions.get(path);
+ if (!mCachedPermissions.containsKey(path)) {
+ mCachedPermissions.put(path, current);
+ changedPermissions.add(path);
+ } else {
+ int previous = mCachedPermissions.get(path);
+ if (previous != current) {
+ mCachedPermissions.put(path, current);
+ changedPermissions.add(path);
+ }
+ }
+ }
+
+ for (String path : changedPermissions) {
+ onPermissionsChange(path);
+ }
+
+
+ }
+
+ //call all the listeners effected by a permission change at this path
+ void onPermissionsChange(String path) {
+ int permission = getPermission(path);
+ if (mNearestAncestors.containsKey(path)) {
+ for (String listenerPath : mNearestAncestors.get(path)) {
+ if (mPermissionValueEventListeners.containsKey(listenerPath)) {
+ for (OnPermissionChangeListener listener : mPermissionValueEventListeners.get(listenerPath)) {
+ listener.onPermissionChange(permission);
+ }
+ }
+ }
+ }
+ }
+
+ private ValueEventListener grantedBlessingListener = new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if (dataSnapshot.exists()) {
+ for (DataSnapshot blessingSnap : dataSnapshot.getChildren()) {
+ Blessing blessing = new Blessing(blessingSnap);
+ mGrantedBlessings.put(blessing.getId(), blessing);
+ }
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ void onBlessingRemoved(DataSnapshot snapshot) {
+ Blessing removedBlessing = mBlessings.remove(snapshot.getKey());
+ refreshPermissions();
+ }
+
+ public Blessing getGrantedBlessing(String target) {
+ return mGrantedBlessings.get(target);
+ }
+
+ static String getNearestCommonAncestor(String path, Set<String> ancestors) {
+ if (path.startsWith("/")) {
+ throw new IllegalArgumentException("Path can't start with /");
+ }
+ if (ancestors.contains(path)) {
+ return path;
+ }
+ String subpath = path;
+ int index;
+ while ((index = subpath.lastIndexOf("/")) != -1) {
+ subpath = subpath.substring(0, index);
+ if (ancestors.contains(subpath)) {
+ return subpath;
+ }
+ }
+
+ return null;
+ }
+
+ //return a blessing interface for granting/revoking permissions
+ public Blessing bless(String target) {
+ Blessing result = getGrantedBlessing(target);
+ if (result == null) {
+ result = new Blessing(target, this.mId, mBlessingsRef.push());
+ mGrantedBlessings.put(target, result);
+ }
+ return result;
+ }
+
+ private ChildEventListener requestListener = new ChildEventListener() {
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String s) {
+ onBlessingUpdated(dataSnapshot);
+ }
+
+ @Override
+ public void onChildChanged(DataSnapshot dataSnapshot, String s) {
+ onBlessingUpdated(dataSnapshot);
+ }
+
+ @Override
+ public void onChildRemoved(DataSnapshot dataSnapshot) {
+ onBlessingRemoved(dataSnapshot);
+ }
+
+ @Override
+ public void onChildMoved(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ private void onRequestUpdated(DataSnapshot snapshot) {
+ if (!snapshot.exists()) return;
+
+ PermissionRequest request = snapshot.getValue(PermissionRequest.class);
+ if (request != null) {
+ mRequests.put(request.getId(), request);
+ //TODO: filter relevant requests
+ for (OnRequestListener listener : requestListeners) {
+ listener.onRequest(request);
+ }
+ }
+ }
+
+ //TODO: only notify listeners that returned true when the request was added
+ private void onRequestRemoved(DataSnapshot snapshot) {
+ mRequests.remove(snapshot.getKey());
+ PermissionRequest request = snapshot.getValue(PermissionRequest.class);
+ if (request != null) {
+ for (OnRequestListener listener : requestListeners) {
+ listener.onRequestRemoved(request);
+ }
+ }
+ }
+
+
+ private ChildEventListener blessingListener = new ChildEventListener() {
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String s) {
+ onBlessingUpdated(dataSnapshot);
+ }
+
+ @Override
+ public void onChildChanged(DataSnapshot dataSnapshot, String s) {
+ onBlessingUpdated(dataSnapshot);
+ }
+
+ @Override
+ public void onChildRemoved(DataSnapshot dataSnapshot) {
+ onBlessingRemoved(dataSnapshot);
+ }
+
+ @Override
+ public void onChildMoved(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ public int getPermission(String path) {
+ if (mCachedPermissions.containsKey(path))
+ return mCachedPermissions.get(path);
+ int result = getCombinedPermission(path);
+ mCachedPermissions.put(path, result);
+ return result;
+ }
+
+ private int getCombinedPermission(String path) {
+ int current = 0;
+ for (Blessing blessing : mBlessings.values()) {
+ current = blessing.getPermissionAt(path, current);
+ }
+ return current;
+ }
+
+ public OnPermissionChangeListener addPermissionEventListener(String path, OnPermissionChangeListener listener) {
+ int current = FLAG_DEFAULT;
+ mPermissionValueEventListeners.put(path, listener);
+
+ String nearestAncestor = getNearestCommonAncestor(path, mCachedPermissions.keySet());
+ if (nearestAncestor != null) {
+ current = getPermission(nearestAncestor);
+ mNearestAncestors.put(nearestAncestor, path);
+ }
+ listener.onPermissionChange(current);
+ return listener;
+ }
+
+ public void removePermissionEventListener(String path, OnPermissionChangeListener listener) {
+ mPermissionValueEventListeners.remove(path, listener);
+
+ String nca = getNearestCommonAncestor(path, mCachedPermissions.keySet());
+ mNearestAncestors.remove(nca, path);
+
+ }
+
+ public void removeOnRequestListener(PermissionManager.OnRequestListener requestListener) {
+ requestListeners.remove(requestListener);
+ }
+
+ public PermissionManager.OnRequestListener addOnRequestListener(PermissionManager.OnRequestListener requestListener) {
+ requestListeners.add(requestListener);
+ return requestListener;
+ }
+
+ public void removeOnReferralListener(String path, OnReferralListener referralListener) {
+ referralListeners.remove(path, referralListener);
+ }
+
+ public OnReferralListener addOnReferralListener(String path, OnReferralListener referralListener) {
+ referralListeners.put(path, referralListener);
+ return referralListener;
+ }
+
+ public void refer(PermissionReferral referral) {
+ }
+
+ public void request(PermissionRequest request) {
+ if (request == null)
+ throw new IllegalArgumentException("null request");
+
+ DatabaseReference requestRef = mRequestsRef.push();
+ request.setId(requestRef.getKey());
+ requestRef.setValue(request);
+ }
+
+ public interface OnRequestListener {
+ boolean onRequest(PermissionRequest request);
+
+ void onRequestRemoved(PermissionRequest request);
+ }
+
+ public interface OnReferralListener {
+ void onReferral();
+ }
+
+ public interface OnPermissionChangeListener {
+ void onPermissionChange(int current);
+
+ void onCancelled(DatabaseError databaseError);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReference.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReference.java
new file mode 100644
index 0000000..566754f
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReference.java
@@ -0,0 +1,38 @@
+// 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.provider.ContactsContract;
+
+import com.google.firebase.database.ChildEventListener;
+import com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.ValueEventListener;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class PermissionReference {
+
+ private DatabaseReference mPermissionReference;
+
+ private final Map<String, Integer> permissions = new HashMap<>(); //key is group path
+
+ public PermissionReference(DatabaseReference root, String path) {
+ this.mPermissionReference = root.child(path).child(PermissionManager.KEY_PERMISSIONS);
+ }
+
+ public void setPermission(int permission) {
+ mPermissionReference.setValue(permission);
+ }
+
+ public void clearPermission() {
+ mPermissionReference.removeValue();
+ }
+
+}
\ No newline at end of file
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReferral.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReferral.java
new file mode 100644
index 0000000..ac10f33
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReferral.java
@@ -0,0 +1,11 @@
+// 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;
+
+/**
+ * Created by phamilton on 7/6/16.
+ */
+public class PermissionReferral {
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionRequest.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionRequest.java
new file mode 100644
index 0000000..e856113
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionRequest.java
@@ -0,0 +1,65 @@
+// 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 java.security.Permission;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by phamilton on 6/28/16.
+ */
+
+//TODO: multiple resources (Request groups)
+public class PermissionRequest {
+
+ private String id;
+ private String source;
+ private Map<String, Integer> permissions = new HashMap<>();
+ private Map<String, String> description= new HashMap<>();
+
+ public PermissionRequest(){}
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public Map<String, Integer> getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(Map<String, Integer> permissions) {
+ this.permissions = permissions;
+ }
+
+ public Map<String, String> getDescription() {
+ return description;
+ }
+
+ public void setDescription(Map<String, String> description) {
+ this.description = description;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public static class Builder{
+ private PermissionRequest request;
+
+ public Builder(String path){
+ this.request = new PermissionRequest();
+ }
+ }
+
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionService.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionService.java
new file mode 100644
index 0000000..756693c
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionService.java
@@ -0,0 +1,488 @@
+// 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.os.Binder;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.util.Log;
+
+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 org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+
+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.Messenger;
+import examples.baku.io.permissions.messenger.Message;
+
+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 int FOCUS_NOTIFICATION = 43254;
+
+ static final String KEY_BLESSINGS = PermissionManager.KEY_BLESSINGS;
+
+ NotificationManager mNotificationManager;
+
+ FirebaseDatabase mFirebaseDB;
+ DatabaseReference mDevicesReference;
+ DatabaseReference mRequestsReference;
+
+
+ DatabaseReference mMessengerReference;
+ Messenger mMessenger;
+
+ DatabaseReference mPermissionsReference;
+ PermissionManager mPermissionManager;
+ Blessing mDeviceBlessing;
+
+ DatabaseReference mLocalDeviceReference;
+
+ private String mDeviceId;
+
+ private IBinder mBinder = new PermissionServiceBinder();
+
+
+ private String mFocus;
+ private Map<String, DeviceData> mDiscovered = new HashMap<>();
+ private Map<String, Integer> mDiscoveredNotifications = new HashMap<>();
+
+ private HashSet<DiscoveryListener> mDiscoveryListener = new HashSet<>();
+
+ public void revokeAll() {
+ for (Blessing blessing : mPermissionManager.mGrantedBlessings.values()) {
+ blessing.revoke(null);
+ }
+ }
+
+ 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");
+
+ mPermissionsReference = mFirebaseDB.getReference("permissions");
+ mPermissionManager = new PermissionManager(mFirebaseDB.getReference(), mDeviceId);
+
+ mPermissionManager.addOnRequestListener(new PermissionManager.OnRequestListener() {
+ @Override
+ public boolean onRequest(PermissionRequest request) {
+ return true;
+ }
+
+ @Override
+ public void onRequestRemoved(PermissionRequest request) {
+
+ }
+ });
+
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ initForegroundNotification();
+
+ registerDevice();
+ initDeviceBlessing();
+ initMessenger();
+ initDiscovery();
+
+ mRunning = true;
+ }
+
+ public Messenger getMessenger() {
+ return mMessenger;
+ }
+
+ public String getFocus() {
+ return mFocus;
+ }
+
+
+ public void setFocus(String dId) {
+ mFocus = 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, 0, dismissIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Notification.Builder notificationBuilder = new Notification.Builder(this)
+ .setContentTitle(title)
+ .setContentText(subtitle)
+ .setSmallIcon(icon)
+ .setVibrate(new long[]{100})
+ .setPriority(Notification.PRIORITY_MAX)
+ .setDeleteIntent(dismissPending);
+
+ Map<String, String> status = device.getStatus();
+ if (status != null && status.containsKey(ComposeActivity.EXTRA_MESSAGE_PATH)) {
+ Intent pullIntent = new Intent(this, ComposeActivity.class);
+ pullIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ pullIntent.putExtra(ComposeActivity.EXTRA_MESSAGE_PATH, status.get(ComposeActivity.EXTRA_MESSAGE_PATH));
+
+ notificationBuilder.addAction(new Notification.Action.Builder(R.drawable.ic_cast_black_24dp, "Pull Message", PendingIntent.getActivity(this, 0, pullIntent, PendingIntent.FLAG_CANCEL_CURRENT)).build());
+ }
+
+ Notification notification = notificationBuilder.build();
+ mNotificationManager.notify(FOCUS_NOTIFICATION, notification);
+ }
+
+
+ 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).child(value);
+ }
+
+ 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, 0, contentIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Intent discoverIntent = new Intent(getApplicationContext(), PermissionService.class);
+ discoverIntent.putExtra("type", "discover");
+ PendingIntent discoverPendingIntent = PendingIntent.getService(this, 1, discoverIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Intent closeIntent = new Intent(getApplicationContext(), PermissionService.class);
+ closeIntent.putExtra("type", "close");
+ PendingIntent closePendingIntent = PendingIntent.getService(this, 2, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Notification notification = new Notification.Builder(this)
+ .setContentIntent(contentPendingIntent)
+ .setSmallIcon(R.drawable.ic_vpn_key_black_24dp)
+ .setContentTitle("Permission service running")
+ .addAction(new Notification.Action.Builder(R.drawable.ic_zoom_in_black_24dp, "Discover", discoverPendingIntent).build())
+ .addAction(new Notification.Action.Builder(R.drawable.ic_close_black_24dp, "Stop", closePendingIntent).build())
+ .build();
+ startForeground(FOREGROUND_NOTIFICATION_ID, notification);
+ }
+
+ void refreshForegroundNotification(Notification notification) {
+ mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
+ }
+
+
+ public void initDeviceBlessing() {
+
+ final DatabaseReference deviceBlessingRef = mFirebaseDB.getReference(KEY_BLESSINGS).child(mDeviceId);
+ deviceBlessingRef.addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if (dataSnapshot.exists()) {
+ mDeviceBlessing = new Blessing(dataSnapshot);
+ } else {
+ mDeviceBlessing = new Blessing(mDeviceId, null, deviceBlessingRef);
+ }
+
+ //give device access its own document directory
+ mDeviceBlessing.setPermissions("documents/" + mDeviceId, PermissionManager.FLAG_WRITE | PermissionManager.FLAG_READ);
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ });
+ }
+
+ public Blessing getDeviceBlessing() {
+ return mDeviceBlessing;
+ }
+
+ public void initMessenger() {
+ mMessengerReference = mFirebaseDB.getReference("messages");
+ mMessenger = new Messenger(mDeviceId, mMessengerReference);
+
+ mMessenger.on("disassociate", new Messenger.Listener() {
+ @Override
+ public void call(String args, Messenger.Ack callback) {
+
+ }
+ });
+
+ mMessenger.on("cast", new Messenger.Listener() {
+ @Override
+ public void call(String args, Messenger.Ack callback) {
+ 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);
+ }
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.hasExtra("type")) {
+ String type = intent.getStringExtra("type");
+ l("start command " + type);
+
+ //TODO: move to a broadcast receiver. StartService intents are not ideal.
+ if ("sendRequest".equals(type)) {
+ if (intent.hasExtra("request")) {
+ Message request = intent.getParcelableExtra("request");
+// sendRequest(request);
+ }
+
+ } 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 ("dismiss".equals(type)) {
+ Message request = new Message("disassociate");
+ request.setTarget(mFocus);
+// sendRequest(request);
+ setFocus(null);
+
+
+ } else if ("close".equals(type)) {
+ stopSelf();
+ } else if ("focus".equals(type)) {
+ if (intent.hasExtra("deviceId")) {
+ String dId = intent.getStringExtra("deviceId");
+ l("targetting " + dId);
+ if (mDiscovered.containsKey(dId)) {
+ mFocus = dId;
+ DeviceData target = mDiscovered.get(dId);
+
+ String title = "Targetting device: " + target.getName();
+
+ Intent contentIntent = new Intent(this, EmailActivity.class);
+
+ Intent discoverIntent = new Intent(this, PermissionService.class);
+ discoverIntent.putExtra("type", "discover");
+ PendingIntent discoverPendingIntent = PendingIntent.getService(this, 0, discoverIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Intent castIntent = new Intent(this, PermissionService.class);
+ castIntent.putExtra("type", "sendRequest");
+ castIntent.putExtra("request", new Message("start"));
+ PendingIntent castPendingIntent = PendingIntent.getService(this, 0, castIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Notification notification = new Notification.Builder(this)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setVibrate(new long[]{100})
+ .setContentIntent(PendingIntent.getActivity(this, 0, contentIntent, 0))
+ .setSmallIcon(R.drawable.ic_vpn_key_black_24dp)
+ .setContentTitle(title)
+ .addAction(new Notification.Action.Builder(R.drawable.ic_cast_black_24dp, "Cast", castPendingIntent).build())
+ .addAction(new Notification.Action.Builder(R.drawable.ic_zoom_in_black_24dp, "Discover", discoverPendingIntent).build())
+ .build();
+
+ refreshForegroundNotification(notification);
+
+ }
+ for (Iterator<Integer> iterator = mDiscoveredNotifications.values().iterator(); iterator.hasNext(); ) {
+ int notId = iterator.next();
+ mNotificationManager.cancel(notId);
+ }
+ mDiscoveredNotifications.clear();
+ }
+ }
+ }
+ 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);
+ }
+ }
+ } catch (DatabaseException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+
+ 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);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DeviceData.java b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DeviceData.java
new file mode 100644
index 0000000..697561b
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DeviceData.java
@@ -0,0 +1,58 @@
+// 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.discovery;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by phamilton on 6/19/16.
+ */
+public class DeviceData {
+
+ private String id;
+ private String name;
+ private Boolean active;
+ private Map<String, String> status = new HashMap<>();
+
+ public DeviceData(){}
+
+ public DeviceData(String id, String name){
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public Map<String, String> getStatus() {
+ return status;
+ }
+
+ public void setStatus(Map<String, String> status) {
+ this.status = status;
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivity.java b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivity.java
new file mode 100644
index 0000000..0ace780
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivity.java
@@ -0,0 +1,117 @@
+// 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.discovery;
+
+import android.app.Fragment;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.Map;
+
+import examples.baku.io.permissions.PermissionService;
+import examples.baku.io.permissions.R;
+import examples.baku.io.permissions.examples.ComposeActivity;
+import examples.baku.io.permissions.util.EventFragment;
+
+public class DevicePickerActivity extends AppCompatActivity implements EventFragment.EventFragmentListener, ServiceConnection {
+
+ private PermissionService mPermissionService;
+ private Map<String, DeviceData> mDevices;
+ private DevicePickerActivityFragment mFragment;
+
+ private int requestCode;
+ public static final int REQUEST_FOCUS = -1;
+ public static final int REQUEST_DEVICE_ID = 2;
+ public static final String EXTRA_REQUEST = "requestCode";
+ public static final String EXTRA_DEVICE_ID = "requestCode";
+ public static final String EXTRA_REQUEST_ARGS = "requestArgs";
+
+ private Intent mIntent;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); //close notification tray
+ setContentView(R.layout.content_device_picker);
+
+ Intent intent = getIntent();
+ if(intent != null){
+ requestCode = intent.getIntExtra(EXTRA_REQUEST, REQUEST_FOCUS);
+ }
+
+ mIntent = getIntent();
+ PermissionService.bind(this);
+ }
+
+ @Override
+ public void onAttachFragment(Fragment fragment) {
+ super.onAttachFragment(fragment);
+ mFragment = (DevicePickerActivityFragment)fragment;
+ mFragment.setDevices(mDevices);
+ }
+
+ @Override
+ public boolean onFragmentEvent(int action, Bundle args, EventFragment fragment) {
+ switch(action){
+ case DevicePickerActivityFragment.EVENT_ITEMCLICKED:
+ String dId = args.getString(DevicePickerActivityFragment.ARG_DEVICE_ID);
+ if(dId != null){
+ if(requestCode == REQUEST_DEVICE_ID){
+ Intent result = new Intent();
+ result.putExtra(EXTRA_DEVICE_ID,dId);
+ if(mIntent != null && mIntent.hasExtra(EXTRA_REQUEST_ARGS))
+ {
+ result.putExtra(EXTRA_REQUEST_ARGS, mIntent.getStringExtra(EXTRA_REQUEST_ARGS));
+ }
+ setResult(0, result);
+ }else{
+ mPermissionService.setFocus(dId);
+ }
+ finish();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mPermissionService = ((PermissionService.PermissionServiceBinder)service).getInstance();
+ mDevices = mPermissionService.getDiscovered();
+ if(mFragment != null){
+ mFragment.setDevices(mDevices);
+ }
+ mPermissionService.addDiscoveryListener(new PermissionService.DiscoveryListener() {
+ @Override
+ public void onChange(Map<String, DeviceData> devices) {
+ if(mFragment != null){
+ mFragment.setDevices(mDevices);
+ }
+ }
+
+ @Override
+ public void onDisassociate(String deviceId) {
+
+ }
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unbindService(this);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivityFragment.java b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivityFragment.java
new file mode 100644
index 0000000..6b2bcd4
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DevicePickerActivityFragment.java
@@ -0,0 +1,126 @@
+// 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.discovery;
+
+import android.os.Bundle;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import examples.baku.io.permissions.R;
+import examples.baku.io.permissions.util.EventFragment;
+
+/**
+ * A placeholder fragment containing a simple view.
+ */
+public class DevicePickerActivityFragment extends EventFragment {
+
+ public static final int EVENT_ITEMCLICKED = 2;
+
+ public static final String ARG_DEVICE_ID = "deviceId";
+
+ LinkedHashMap<String,DeviceData> devices = new LinkedHashMap<>();
+ DeviceListAdapter mAdapter;
+ RecyclerView mDeviceRecycler;
+ LinearLayoutManager mLayoutManager;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_device_picker, container, false);
+ mAdapter = new DeviceListAdapter();
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ mDeviceRecycler = (RecyclerView) view.findViewById(R.id.deviceRecyclerView);
+ mDeviceRecycler.setLayoutManager(mLayoutManager);
+ mDeviceRecycler.setAdapter(mAdapter);
+ return view;
+ }
+
+ public void setDevices(Map<String,DeviceData> devices){
+ if(devices != null){
+ this.devices = new LinkedHashMap<>(devices);
+ }else{
+ this.devices.clear();
+ }
+ if(mAdapter != null){
+ this.mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public CardView mCardView;
+ public ViewHolder(CardView v) {
+ super(v);
+ mCardView = v;
+
+ mCardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ }
+ });
+ }
+ }
+
+ class DeviceListAdapter extends RecyclerView.Adapter<ViewHolder> {
+
+ public DeviceData getItem(int position) {
+ List<String> order = new ArrayList<>(devices.keySet());
+ return devices.get(order.get(position));
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ // create a new view
+ CardView v = (CardView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.device_card_item, parent, false);
+ // set the view's size, margins, paddings and layout parameters
+ ViewHolder vh = new ViewHolder(v);
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+
+ final DeviceData item = getItem(position);
+
+ String title = item.getName();
+ if (title != null) {
+ TextView titleView = (TextView) holder.mCardView.findViewById(R.id.card_title);
+ titleView.setText(title);
+ }
+
+ holder.mCardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle args = new Bundle();
+ args.putString(ARG_DEVICE_ID, item.getId());
+ onEvent(EVENT_ITEMCLICKED, args);
+// Intent intent = new Intent(EmailActivity.this, ComposeActivity.class);
+// intent.putExtra(ComposeActivity.EXTRA_MESSAGE_ID, item.getId());
+// startActivityForResult(intent, 0);
+ }
+ });
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return devices.size();
+ }
+ }
+
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/examples/ComposeActivity.java b/permissions/app/src/main/java/examples/baku/io/permissions/examples/ComposeActivity.java
new file mode 100644
index 0000000..a8b23cc
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/examples/ComposeActivity.java
@@ -0,0 +1,333 @@
+// 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.examples;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.TextInputLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.ValueEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import examples.baku.io.permissions.PermissionManager;
+import examples.baku.io.permissions.discovery.DeviceData;
+import examples.baku.io.permissions.PermissionService;
+import examples.baku.io.permissions.R;
+import examples.baku.io.permissions.discovery.DevicePickerActivity;
+import examples.baku.io.permissions.synchronization.SyncText;
+
+public class ComposeActivity extends AppCompatActivity implements ServiceConnection {
+
+ public final static String EXTRA_MESSAGE_ID = "messageId";
+ public final static String EXTRA_MESSAGE_PATH = "messagePath";
+
+ private String mPath;
+
+ private String mOwner;
+ private String mDeviceId;
+ private String mId;
+ private PermissionService mPermissionService;
+ private DatabaseReference mMessageRef;
+ private DatabaseReference mSyncedMessageRef;
+
+ String sourceId;
+
+
+ EditText mToText;
+ EditText mFrom;
+ EditText mSubject;
+ EditText mMessage;
+
+ TextInputLayout mToLayout;
+ TextInputLayout mFromLayout;
+ TextInputLayout mSubjectLayout;
+ TextInputLayout mMessageLayout;
+
+
+ Map<String, Integer> permissions = new HashMap<>();
+
+ HashMap<String, SyncText> syncTexts = new HashMap<>();
+
+ HashMap<String, ValueEventListener> listeners = new HashMap<>();
+ HashMap<String, DataSnapshot> mSnapshots = new HashMap<>();
+ DataSnapshot currentSnapshot;
+
+ String original;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_compose);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ toolbar.setTitle("Compose Message");
+
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ sendMessage();
+ }
+ });
+
+
+ mToText = (EditText) findViewById(R.id.composeTo);
+ mToLayout = (TextInputLayout) findViewById(R.id.composeToLayout);
+
+ mFrom = (EditText) findViewById(R.id.composeFrom);
+ mFromLayout = (TextInputLayout) findViewById(R.id.composeFromLayout);
+
+ mSubject = (EditText) findViewById(R.id.composeSubject);
+ mSubjectLayout = (TextInputLayout) findViewById(R.id.composeSubjectLayout);
+
+ mMessage = (EditText) findViewById(R.id.composeMessage);
+ mMessageLayout = (TextInputLayout) findViewById(R.id.composeMessageLayout);
+
+ bindService(new Intent(this, PermissionService.class), this, Service.BIND_AUTO_CREATE);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_compose, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_send) {
+ sendMessage();
+ } else if (id == R.id.action_cast) {
+ if (mPermissionService != null) {
+ Intent requestIntent = new Intent(ComposeActivity.this, DevicePickerActivity.class);
+ requestIntent.putExtra(DevicePickerActivity.EXTRA_REQUEST, DevicePickerActivity.REQUEST_DEVICE_ID);
+ requestIntent.putExtra(DevicePickerActivity.EXTRA_REQUEST_ARGS, mPath);
+ startActivityForResult(requestIntent, DevicePickerActivity.REQUEST_DEVICE_ID);
+ }
+
+ } else if (id == R.id.action_settings) {
+
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ void sendMessage() {
+
+ //TODO: PermissionManager.request()
+
+ finish();
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == DevicePickerActivity.REQUEST_DEVICE_ID && data != null && data.hasExtra(DevicePickerActivity.EXTRA_DEVICE_ID)) {
+ String focus = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ID);
+ mPermissionService.getPermissionManager().bless(focus)
+ .setPermissions(mPath, PermissionManager.FLAG_READ)
+ .setPermissions(mPath + "/message", PermissionManager.FLAG_WRITE)
+ .setPermissions(mPath + "/subject", PermissionManager.FLAG_WRITE);
+ JSONObject castArgs = new JSONObject();
+ try {
+ castArgs.put("activity", ComposeActivity.class.getSimpleName());
+ castArgs.put(EXTRA_MESSAGE_PATH, mPath);
+ mPermissionService.getMessenger().to(focus).emit("cast", castArgs.toString());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ PermissionService.PermissionServiceBinder binder = (PermissionService.PermissionServiceBinder) service;
+ mPermissionService = binder.getInstance();
+
+
+ if (mPermissionService != null) {
+ mDeviceId = mPermissionService.getDeviceId();
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ if (intent.hasExtra(EXTRA_MESSAGE_PATH)) {
+ mPath = intent.getStringExtra(EXTRA_MESSAGE_PATH);
+ String[] pathElements = mPath.split("/");
+ mId = pathElements[pathElements.length - 1];
+ } else if (intent.hasExtra(EXTRA_MESSAGE_ID)) {
+ mId = intent.getStringExtra(EXTRA_MESSAGE_ID);
+ mPath = EmailActivity.KEY_DOCUMENTS
+ + "/" + mDeviceId
+ + "/" + EmailActivity.KEY_EMAILS
+ + "/" + EmailActivity.KEY_MESSAGES
+ + "/" + mId;
+ }
+ }
+
+ if (mPath == null) {
+ mId = UUID.randomUUID().toString();
+ mPath = "documents/" + mDeviceId + "/emails/messages/" + mId;
+ }
+
+ mMessageRef = mPermissionService.getFirebaseDB().getReference(mPath);
+ mSyncedMessageRef = mMessageRef.child("syncedValues");
+ mPermissionService.getPermissionManager().addPermissionEventListener(mPath, messagePermissionListener);
+ wrapTextField(mToLayout, "to");
+ wrapTextField(mFromLayout, "from");
+ wrapTextField(mSubjectLayout, "subject");
+ wrapTextField(mMessageLayout, "message");
+
+ mPermissionService.setStatus(EXTRA_MESSAGE_PATH, mPath);
+
+ }
+ }
+
+ PermissionManager.OnPermissionChangeListener messagePermissionListener = new PermissionManager.OnPermissionChangeListener() {
+ @Override
+ public void onPermissionChange(int current) {
+ if (current > 0) {
+ mMessageRef.child("id").setValue(mId);
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+
+ void wrapTextField(final TextInputLayout editContainer, final String key) {
+ final EditText edit = editContainer.getEditText();
+
+ mPermissionService.getPermissionManager().addPermissionEventListener(mPath + "/" + key, new PermissionManager.OnPermissionChangeListener() {
+ @Override
+ public void onPermissionChange(int current) {
+ Log.e(key, "::" + current);
+ if ((current & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
+ edit.setEnabled(true);
+ edit.setOnClickListener(null);
+ edit.setFocusable(true);
+ edit.setBackgroundColor(Color.TRANSPARENT);
+ linkTextField(edit, key);
+ } else if ((current & PermissionManager.FLAG_READ) == PermissionManager.FLAG_READ) {
+ edit.setEnabled(false);
+ edit.setOnClickListener(null);
+ edit.setFocusable(false);
+ edit.setBackgroundColor(Color.TRANSPARENT);
+ linkTextField(edit, key);
+ } else {
+ unlinkTextField(key);
+ edit.setEnabled(false);
+ edit.setFocusable(false);
+ edit.setBackgroundColor(Color.BLACK);
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ });
+ }
+
+ void unlinkTextField(String key) {
+ if (syncTexts.containsKey(key)) {
+ syncTexts.get(key).unlink();
+ }
+ }
+
+ void linkTextField(final EditText edit, final String key) {
+ final SyncText syncText = new SyncText(mSyncedMessageRef.child(key), mMessageRef.child(key));
+ syncTexts.put(key, syncText);
+
+ syncText.setOnTextChangeListener(new SyncText.OnTextChangeListener() {
+ @Override
+ public void onTextChange(final String currentText) {
+ final int sel = Math.min(edit.getSelectionStart(), currentText.length());
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ edit.setText(currentText);
+ if (sel > -1) {
+ edit.setSelection(sel);
+ }
+ }
+ });
+ }
+ });
+
+ edit.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ syncText.update(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unlinkTextField("to");
+ unlinkTextField("form");
+ unlinkTextField("subject");
+ unlinkTextField("message");
+ if (mPermissionService != null) {
+ mPermissionService.revokeAll();
+ }
+ unbindService(this);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/examples/EmailActivity.java b/permissions/app/src/main/java/examples/baku/io/permissions/examples/EmailActivity.java
new file mode 100644
index 0000000..db2a7f4
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/examples/EmailActivity.java
@@ -0,0 +1,351 @@
+// 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.examples;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.google.common.collect.Iterables;
+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 org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import examples.baku.io.permissions.PermissionService;
+import examples.baku.io.permissions.R;
+import examples.baku.io.permissions.discovery.DevicePickerActivity;
+
+public class EmailActivity extends AppCompatActivity implements ServiceConnection {
+
+
+ private static final String TAG = PermissionService.class.getSimpleName();
+
+ static void l(String msg) {
+ Log.e(TAG, msg);
+ } //TODO: real logging
+
+ public static final String KEY_DOCUMENTS = "documents";
+ public static final String KEY_EMAILS = "emails";
+ public static final String KEY_MESSAGES = "messages";
+
+ private PermissionService mPermissionService;
+ private String mDeviceId;
+ private FirebaseDatabase mFirebaseDB;
+ private DatabaseReference mMessagesRef;
+
+ private RecyclerView mInboxRecyclerView;
+ private MessagesAdapter mInboxAdapter;
+ private LinearLayoutManager mLayoutManager;
+
+ private LinkedHashMap<String, MessageData> mMessages = new LinkedHashMap<>();
+
+ private ArrayList<String> mMessageOrder = new ArrayList<>();
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_permission);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ if (fab != null) {
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startActivity(new Intent(EmailActivity.this, ComposeActivity.class));
+// mPermissionService.getDeviceBlessing().setPermissions("documents/" + mDeviceId +"/snake", new Random().nextInt());
+
+ }
+ });
+ }
+
+ PermissionService.start(this);
+ PermissionService.bind(this);
+
+ mInboxAdapter = new MessagesAdapter(mMessages);
+ mLayoutManager = new LinearLayoutManager(this);
+ mInboxRecyclerView = (RecyclerView) findViewById(R.id.inboxRecyclerView);
+ mInboxRecyclerView.setLayoutManager(mLayoutManager);
+ mInboxRecyclerView.setAdapter(mInboxAdapter);
+
+ ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
+
+ itemTouchHelper.attachToRecyclerView(mInboxRecyclerView);
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_permission, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_cast) {
+
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT) {
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
+ return false;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
+ //Remove swiped item from list and notify the RecyclerView
+ int pos = viewHolder.getAdapterPosition();
+ MessageData item = mInboxAdapter.getItem(pos);
+ if (item != null) {
+ mMessagesRef.child(item.getId()).removeValue();
+ }
+ }
+ };
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ PermissionService.PermissionServiceBinder binder = (PermissionService.PermissionServiceBinder) service;
+ mPermissionService = binder.getInstance();
+ if (mPermissionService != null) {
+ mDeviceId = mPermissionService.getDeviceId();
+ mFirebaseDB = mPermissionService.getFirebaseDB();
+ mMessagesRef = mFirebaseDB.getReference(KEY_DOCUMENTS).child(mDeviceId).child(KEY_EMAILS).child(KEY_MESSAGES);
+ mMessagesRef.addValueEventListener(messagesValueListener);
+ mMessagesRef.addChildEventListener(messageChildListener);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+
+
+ private ChildEventListener messageChildListener = new ChildEventListener() {
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String s) {
+ onMessageUpdated(dataSnapshot);
+ mInboxAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onChildChanged(DataSnapshot dataSnapshot, String s) {
+ onMessageUpdated(dataSnapshot);
+ mInboxAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onChildRemoved(DataSnapshot dataSnapshot) {
+ onMessageRemoved(dataSnapshot.getKey());
+ mInboxAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onChildMoved(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ private ValueEventListener messagesValueListener = new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ onMessagesUpdated(dataSnapshot);
+ mInboxAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ void onMessagesUpdated(DataSnapshot snapshot) {
+ if (snapshot == null) throw new IllegalArgumentException("null snapshot");
+
+ mMessages.clear();
+ for (DataSnapshot snap : snapshot.getChildren()) {
+ onMessageUpdated(snap);
+ }
+ }
+
+ void onMessageUpdated(DataSnapshot snapshot) {
+ try {
+ MessageData msg = snapshot.getValue(MessageData.class);
+ String key = msg.getId();
+ mMessages.put(key, msg);
+ } catch (DatabaseException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void onMessageRemoved(String id) {
+ mMessages.remove(id);
+ }
+
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public CardView mCardView;
+
+ public ViewHolder(CardView v) {
+ super(v);
+ mCardView = v;
+
+ mCardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == DevicePickerActivity.REQUEST_DEVICE_ID && data != null && data.hasExtra(DevicePickerActivity.EXTRA_DEVICE_ID)) {
+ String focus = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ID);
+ String path = data.getStringExtra(DevicePickerActivity.EXTRA_REQUEST_ARGS);
+
+ mPermissionService.getPermissionManager().bless(focus)
+ .setPermissions(path, 3);
+ JSONObject castArgs = new JSONObject();
+ try {
+ castArgs.put("activity", ComposeActivity.class.getSimpleName());
+ castArgs.put(ComposeActivity.EXTRA_MESSAGE_PATH, path);
+ mPermissionService.getMessenger().to(focus).emit("cast", castArgs.toString());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public class MessagesAdapter extends RecyclerView.Adapter<ViewHolder> {
+ private LinkedHashMap<String, MessageData> mDataset;
+
+ public MessagesAdapter(LinkedHashMap<String, MessageData> dataset) {
+ setDataset(dataset);
+ }
+
+ public void setDataset(LinkedHashMap<String, MessageData> mDataset) {
+ this.mDataset = mDataset;
+ }
+
+ public MessageData getItem(int position) {
+ return Iterables.get(mDataset.values(), position);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ // create a new view
+ CardView v = (CardView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.inbox_card_item, parent, false);
+ // set the view's size, margins, paddings and layout parameters
+ ViewHolder vh = new ViewHolder(v);
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+
+ final MessageData item = getItem(position);
+
+ String title = item.getFrom();
+ if (title != null) {
+ TextView titleView = (TextView) holder.mCardView.findViewById(R.id.card_title);
+ titleView.setText(item.getFrom());
+ TextView subtitleView = (TextView) holder.mCardView.findViewById(R.id.card_subtitle);
+ subtitleView.setText(item.getSubject());
+ }
+
+ ImageView castButton = (ImageView) holder.mCardView.findViewById(R.id.card_trailing);
+ castButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ //choose device
+ Intent requestIntent = new Intent(EmailActivity.this, DevicePickerActivity.class);
+ String path = EmailActivity.KEY_DOCUMENTS
+ + "/" + mDeviceId
+ + "/" + EmailActivity.KEY_EMAILS
+ + "/" + EmailActivity.KEY_MESSAGES
+ + "/" + item.getId();
+ requestIntent.putExtra(DevicePickerActivity.EXTRA_REQUEST, DevicePickerActivity.REQUEST_DEVICE_ID);
+ requestIntent.putExtra(DevicePickerActivity.EXTRA_REQUEST_ARGS, path);
+ startActivityForResult(requestIntent, DevicePickerActivity.REQUEST_DEVICE_ID);
+ }
+ });
+
+ holder.mCardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(EmailActivity.this, ComposeActivity.class);
+ intent.putExtra(ComposeActivity.EXTRA_MESSAGE_ID, item.getId());
+ startActivityForResult(intent, 0);
+ }
+ });
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDataset.size();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unbindService(this);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/examples/InboxFragment.java b/permissions/app/src/main/java/examples/baku/io/permissions/examples/InboxFragment.java
new file mode 100644
index 0000000..fe2a764
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/examples/InboxFragment.java
@@ -0,0 +1,33 @@
+// 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.examples;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import examples.baku.io.permissions.R;
+import examples.baku.io.permissions.util.EventFragment;
+
+/**
+ * A placeholder fragment containing a simple view.
+ */
+public class InboxFragment extends EventFragment {
+
+ public InboxFragment(){}
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.inbox_list, container, false);
+
+
+ return view;
+ }
+
+
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/examples/MessageData.java b/permissions/app/src/main/java/examples/baku/io/permissions/examples/MessageData.java
new file mode 100644
index 0000000..12d2793
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/examples/MessageData.java
@@ -0,0 +1,83 @@
+// 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.examples;
+
+import com.google.firebase.database.ServerValue;
+
+import java.util.Map;
+
+/**
+ * Created by phamilton on 6/22/16.
+ */
+public class MessageData {
+
+ String id;
+ String to = "";
+ String from = "";
+ String subject = "";
+ String message = "";
+ long timeStamp;
+
+// Map<String, Map<String, Integer>> shared = new HashMap<>();
+
+ public MessageData(){}
+
+ public MessageData(String id, String to, String from, String subject, String message) {
+ this.id = id;
+ this.to = to;
+ this.from = from;
+ this.subject = subject;
+ this.message = message;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getTo() {
+ return to;
+ }
+
+ public void setTo(String to) {
+ this.to = to != null ? to : "";
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public void setFrom(String from) {
+ this.from = from != null ? from : "";
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject != null ? subject : "";
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message != null ? message : "";
+ }
+
+
+ public Map<String,String> getTimeStamp() {
+ return ServerValue.TIMESTAMP;
+ }
+
+ public void setTimeStamp(long timeStamp) {
+ this.timeStamp = timeStamp;
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Message.java b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Message.java
new file mode 100644
index 0000000..62eb995
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Message.java
@@ -0,0 +1,142 @@
+// 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.messenger;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Created by phamilton on 6/19/16.
+ */
+public class Message implements Parcelable{
+ private String id;
+ private String parent;
+ private String type;
+ private String target;
+ private String source;
+ private String message;
+ private boolean callback;
+
+ public Message(){}
+
+ public Message(String type) {
+ this(type, null);
+ }
+
+ public Message(String type, String message){
+ this.id = UUID.randomUUID().toString();
+ this.type = type;
+ this.message = message;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ public String getTarget() {
+ return target;
+ }
+
+ public void setTarget(String target) {
+ this.target = target;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public boolean isCallback() {
+ return callback;
+ }
+
+ public void setCallback(boolean callback) {
+ this.callback = callback;
+ }
+
+
+// public Message getChildInstance(){
+// Message result = new Message();
+// result.id = UUID.randomUUID().toString();
+// result.parent = this.id;
+// result.type = this.type;
+// result.target = this.target;
+// result.source = this.source;
+// result.message = this.message;
+// result.callback = this.callback;
+// return result;
+// }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(type);
+ dest.writeString(target);
+ dest.writeString(message);
+ dest.writeByte((byte) (callback ? 1 : 0));
+ }
+
+ private Message(Parcel in) {
+ this.id = in.readString();
+ this.type = in.readString();
+ this.target = in.readString();
+ this.message = in.readString();
+ this.callback = in.readByte() != 0;
+
+ }
+
+ public static final Creator<Message> CREATOR
+ = new Creator<Message>() {
+ @Override
+ public Message createFromParcel(Parcel source) {
+ return new Message(source);
+ }
+
+ @Override
+ public Message[] newArray(int size) {
+ return new Message[0];
+ }
+
+ };
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java
new file mode 100644
index 0000000..faef35f
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java
@@ -0,0 +1,204 @@
+// 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.messenger;
+
+import android.provider.ContactsContract;
+import android.widget.NumberPicker;
+
+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.ValueEventListener;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by phamilton on 6/28/16.
+ *
+ * !!!!!!!!!FOR PROTOTYPING ONLY!!!!!!!
+ * Messaging on top of Firebase real-time database.
+ * Handles single target messaging only.
+ * Stand in for something like socket.io.
+ */
+public class Messenger implements ChildEventListener {
+
+ static final String KEY_TARGET = "target";
+
+ static final String KEY_GROUPS = "_groups";
+
+ private String mId;
+ private DatabaseReference mReference;
+
+ //hacky client side grouping
+ private DatabaseReference mGroupsReference;
+ private DataSnapshot mGroups;
+
+ final private Map<String, Listener> mListeners = new HashMap<>();
+ final private Map<String, Ack> mCallbacks = new HashMap<>();
+
+ public Messenger(String id, DatabaseReference reference) {
+ this.mId = id;
+ this.mReference = reference;
+ this.mReference.orderByChild(KEY_TARGET).equalTo(mId).addChildEventListener(this);
+
+ this.mGroupsReference = mReference.child(KEY_GROUPS);
+ this.mGroupsReference.addValueEventListener(groupsListener);
+ }
+
+ public Emitter to(final String target){
+ return new Emitter() {
+ @Override
+ public void emit(String event, String msg, Ack callback) {
+ if(event == null) throw new IllegalArgumentException("event argument can't be null.");
+
+ Message message = new Message(event, msg);
+ message.setSource(mId);
+
+ if(callback != null){
+ mCallbacks.put(message.getId(), callback);
+ message.setCallback(true);
+ }
+
+ if(mGroups == null || mGroups.hasChild(target)){
+ message.setTarget(target);
+ mReference.child(message.getId()).setValue(message);
+ }else if(mGroups.exists()){
+// DataSnapshot members = mGroups.child(target);
+// if(members.exists()){
+// for(Iterator<DataSnapshot> iterator = members.getChildren().iterator(); iterator.hasNext();){
+// String subTarget = iterator.next().getKey();
+// Message childMessage = message.getChildInstance();
+// childMessage.setTarget(subTarget);
+// mReference.child(childMessage.getId()).setValue(childMessage);
+// }
+// }
+ }
+ }
+ };
+ }
+
+ public void on(String event, Listener listener){
+ if(listener == null){//remove current
+ off(event);
+ }else{
+ mListeners.put(event, listener);
+ }
+ }
+
+ public void off(String event){
+ if(mListeners.containsKey(event)){
+ mListeners.remove(event);
+ }
+ }
+
+ public void join(String group){
+ mGroupsReference.child(group).child(mId).setValue(0);
+ }
+
+ public void leave(String group){
+ mGroupsReference.child(group).child(mId).removeValue();
+ }
+
+ ValueEventListener groupsListener = new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String s) {
+ Message message = null;
+ try{
+ message = dataSnapshot.getValue(Message.class);
+
+ }catch(DatabaseException e){
+ e.printStackTrace();
+ }
+
+ if(message!= null){
+ handleMessage(message);
+ }
+
+ //remove from database.
+ dataSnapshot.getRef().removeValue();
+ }
+
+ private boolean handleMessage(final Message message) {
+ String event = message.getType();
+ if(mListeners.containsKey(event)){
+ Ack callback = null;
+ if(message.isCallback()){
+ //route response to sending messenger
+ callback = new Ack() {
+ @Override
+ public void call(String args) {
+ to(message.getSource()).emit(message.getId(), args);
+ }
+ };
+ }
+ mListeners.get(event).call(message.getMessage(), callback);
+ return true;
+
+ //assume that none of the event listeners match the uuid of a message
+ }else if(mCallbacks.containsKey(event)){
+ mCallbacks.get(event).call(message.getMessage());
+ mCallbacks.remove(event);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onChildChanged(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onChildRemoved(DataSnapshot dataSnapshot) {
+
+ }
+
+ @Override
+ public void onChildMoved(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+ databaseError.toException().printStackTrace();
+ }
+
+ public void disconnect(){
+ mReference.removeEventListener(this);
+ }
+
+ public abstract class Emitter{
+ public void emit(String event, String msg){
+ emit(event, msg, null);
+ }
+
+ abstract public void emit(String event, String msg, Ack callback);
+ }
+
+ public interface Listener{
+ void call(String args, Ack callback);
+ }
+
+ public interface Ack{
+ void call(String args);
+ }
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java
new file mode 100644
index 0000000..7cc639c
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java
@@ -0,0 +1,268 @@
+// 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.synchronization;
+
+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.MutableData;
+import com.google.firebase.database.Transaction;
+import com.google.firebase.database.ValueEventListener;
+
+import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
+
+import java.util.LinkedList;
+import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Created by phamilton on 6/24/16.
+ */
+public class SyncText {
+
+ static final String KEY_CURRENT = "current";
+ static final String KEY_TEXT = "value";
+ static final String KEY_VERSION = "version";
+ static final String KEY_PATCHES = "patches";
+ static final String KEY_SUBSCRIBERS = "subscribers";
+
+ private String text = "";
+ private int ver;
+ private String original = text;
+ private BlockingQueue<SyncTextPatch> mPatchQueue;
+
+ private DiffMatchPatch diffMatchPatch = new DiffMatchPatch();
+
+ private DatabaseReference mSyncRef;
+ private DatabaseReference mPatchesRef;
+ private DatabaseReference mOutputRef;
+
+ private PatchConsumer mPatchConsumer;
+
+ private OnTextChangeListener mOnTextChangeListener;
+
+ private String mId;
+
+ private String mInstance;
+
+
+ public SyncText(DatabaseReference reference, DatabaseReference output){
+ if(reference == null) throw new IllegalArgumentException("null reference");
+
+ mInstance = UUID.randomUUID().toString();
+
+ mSyncRef = reference;
+ mOutputRef = output;
+
+ mId = UUID.randomUUID().toString();
+
+ mPatchQueue = new LinkedBlockingQueue<>();
+ mPatchConsumer = new PatchConsumer(mPatchQueue);
+
+ new Thread(mPatchConsumer).start();
+
+ link();
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public int getVer() {
+ return ver;
+ }
+
+ public void setVer(int ver) {
+ this.ver = ver;
+ }
+
+ public void setOnTextChangeListener(OnTextChangeListener onTextChangeListener) {
+ this.mOnTextChangeListener = onTextChangeListener;
+ }
+
+ public void update(String newText){
+ if(mPatchesRef == null){
+ throw new RuntimeException("database connection hasn't been initialized");
+ }
+
+ LinkedList<DiffMatchPatch.Patch> patches = diffMatchPatch.patchMake(text, newText);
+
+ if(patches.size() > 0){
+ String patchString = diffMatchPatch.patchToText(patches);
+ SyncTextPatch patch = new SyncTextPatch();
+ patch.setVer(ver + 1);
+ patch.setPatch(patchString);
+ mPatchesRef.push().setValue(patch);
+ }
+ }
+
+ private void processPatch(SyncTextPatch patch){
+
+ int v = patch.getVer();
+ if(this.ver >= v){ //ignore patches for previous versions
+ return;
+ }
+
+ LinkedList<DiffMatchPatch.Patch> remotePatch = new LinkedList<>(diffMatchPatch.patchFromText(patch.getPatch()));
+ Object[] results = diffMatchPatch.patchApply(remotePatch, this.text);
+ //TODO: check results
+ if(results != null && results.length > 0 && results[0] instanceof String){
+ String patchedString = (String)results[0];
+ this.ver = v;
+ this.text = patchedString;
+ updateCurrent();
+ }
+ }
+
+ private void updateCurrent(){
+ mSyncRef.child(KEY_CURRENT).runTransaction(new Transaction.Handler() {
+ @Override
+ public Transaction.Result doTransaction(MutableData currentData) {
+ if(currentData.getValue() == null){
+ currentData.child(KEY_TEXT).setValue(text);
+ currentData.child(KEY_VERSION).setValue(ver);
+ }else{
+ int latest = currentData.child(KEY_VERSION).getValue(Integer.class);
+ if(latest > ver){
+ return Transaction.abort();
+ }
+ currentData.child(KEY_TEXT).setValue(text);
+ currentData.child(KEY_VERSION).setValue(ver);
+ }
+ return Transaction.success(currentData);
+ }
+
+ @Override
+ public void onComplete(DatabaseError databaseError, boolean success, DataSnapshot dataSnapshot) {
+ if(success){
+ if(mOnTextChangeListener != null){
+ mOnTextChangeListener.onTextChange(text);
+ }
+ if(mOutputRef != null){ //pass successful change to output location
+ mOutputRef.setValue(text);
+ }
+ }
+ }
+ });
+ }
+
+ public void link(){
+
+ mSyncRef.child(KEY_SUBSCRIBERS).child(mId).setValue(0);
+
+ mPatchesRef = mSyncRef.child(KEY_PATCHES);
+ mSyncRef.child(KEY_CURRENT).addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if(dataSnapshot.exists()){
+ text = dataSnapshot.child(KEY_TEXT).getValue(String.class);
+ ver = dataSnapshot.child(KEY_VERSION).getValue(Integer.class);
+ }else if(mOutputRef != null){ //check if output ref already has a value
+ mOutputRef.addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if(dataSnapshot.exists() && dataSnapshot.getValue() != null){
+ text = dataSnapshot.getValue(String.class);
+ original = text;
+ }
+ updateCurrent();
+ }
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ });
+ }else{ //version 0, empty string
+ updateCurrent();
+ }
+
+// mPatchesRef.orderByChild(KEY_VERSION).startAt(ver).addChildEventListener(mPatchListener);
+ mPatchesRef.addChildEventListener(mPatchListener);
+
+ if(mOnTextChangeListener != null){
+ mOnTextChangeListener.onTextChange(text);
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ });
+ }
+
+ public String getOriginal() {
+ return original;
+ }
+
+ private ChildEventListener mPatchListener = new ChildEventListener() {
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String s) {
+ if(dataSnapshot.exists()){
+ try{
+ SyncTextPatch patch = dataSnapshot.getValue(SyncTextPatch.class);
+ if(patch != null){
+ mPatchQueue.add(patch);
+ }
+ }catch(DatabaseException e){
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ @Override
+ public void onChildChanged(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onChildRemoved(DataSnapshot dataSnapshot) {
+
+ }
+
+ @Override
+ public void onChildMoved(DataSnapshot dataSnapshot, String s) {
+
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ };
+
+ public void unlink(){
+ mSyncRef.child(KEY_PATCHES).removeEventListener(mPatchListener);
+ }
+
+ public interface OnTextChangeListener{
+ void onTextChange(String currentText);
+ }
+
+ private class PatchConsumer implements Runnable {
+ private final BlockingQueue<SyncTextPatch> queue;
+
+ PatchConsumer(BlockingQueue q) { queue = q; }
+ public void run() {
+ try {
+ while (true) { consume(queue.take()); }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ void consume(SyncTextPatch patch) {
+ processPatch(patch);
+ }
+ }
+
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java
new file mode 100644
index 0000000..48ef0b6
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java
@@ -0,0 +1,43 @@
+// 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.synchronization;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Created by phamilton on 6/24/16.
+ */
+public class SyncTextPatch{
+ private int ver;
+ private String patch;
+ private String source;
+
+ public SyncTextPatch() {}
+
+ public int getVer() {
+ return ver;
+ }
+
+ public void setVer(int ver) {
+ this.ver = ver;
+ }
+
+ public String getPatch() {
+ return patch;
+ }
+
+ public void setPatch(String patch) {
+ this.patch = patch;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+}
+
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java b/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java
new file mode 100644
index 0000000..6d50328
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java
@@ -0,0 +1,39 @@
+// 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.util;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+
+/**
+ * Created by phamilton on 6/26/16.
+ *
+ * Fragment that requires the binding context to implement an event handler.
+ */
+public class EventFragment extends Fragment{
+
+ EventFragmentListener mListener;
+
+ public interface EventFragmentListener{
+ boolean onFragmentEvent(int action, Bundle args, EventFragment fragment);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ try {
+ mListener = (EventFragmentListener) context;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(context.toString() + " must implement EventFragmentListener");
+ }
+ }
+
+ public boolean onEvent(int action, Bundle args){
+ if(mListener == null)
+ return false;
+ return mListener.onFragmentEvent(action, args, this);
+ }
+}
diff --git a/permissions/gradle/wrapper/gradle-wrapper.jar b/permissions/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/permissions/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/permissions/gradle/wrapper/gradle-wrapper.properties b/permissions/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..122a0dc
--- /dev/null
+++ b/permissions/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/permissions/gradlew b/permissions/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/permissions/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"