Restructured the blessings(permission groups) architecture to have proper inheritance.
The bless() method must now have a root Blessing
and any changes in a blessings permissions effects all of its children.
Hooked up permission requests. An application instance can request specific permissions
using PermissionManager.request(Permission request). Those requests can be handled by
applications listen using :
PermissionManager.addOnRequestListener(PermissionManager.OnRequestListener)
The current implementation generates a notification for each request.
Added a public blessing to each email message.
Added pull message action to the discovered device notifications.
Integrated the iconify library for handling material design icons.
This removes the need to import a set of pngs for each icon.
Change-Id: I1ce61f43d2e84a70b3066e0e9c6f85a935721d2b
diff --git a/permissions/README.md b/permissions/README.md
index 5b21dd3..a839df5 100644
--- a/permissions/README.md
+++ b/permissions/README.md
@@ -1,6 +1,9 @@
# permission
#Setup
-This application requires firebase. Set it up by following these instructions (https://firebase.google.com/docs/android/setup)
-1. add application package to a project in the Firebase console (https://firebase.google.com/)
-2. add the generated google-services.json to the /app folder.
+
+This application requires Firebase. Set up Firebase for your Android app by
+following these instructions: https://firebase.google.com/docs/android/setup
+
+1. Add application package to a project in the Firebase console (https://console.firebase.google.com/)
+2. Add the generated `google-services.json` to the /app folder.
diff --git a/permissions/app/build.gradle b/permissions/app/build.gradle
index 97a1af4..1908cda 100644
--- a/permissions/app/build.gradle
+++ b/permissions/app/build.gradle
@@ -6,7 +6,7 @@
defaultConfig {
applicationId "examples.baku.io.permissions"
- minSdkVersion 22
+ minSdkVersion 23
targetSdkVersion 23
versionCode 1
versionName "1.0"
@@ -32,6 +32,7 @@
compile 'com.android.support:recyclerview-v7:23.4.0'
compile 'com.google.guava:guava:19.0'
compile 'org.bitbucket.cowwoc.diff-match-patch:diff-match-patch:1.0'
+ compile 'com.joanzapata.iconify:android-iconify-material:2.2.2'
}
diff --git a/permissions/app/src/main/AndroidManifest.xml b/permissions/app/src/main/AndroidManifest.xml
index 5eda784..4e5b397 100644
--- a/permissions/app/src/main/AndroidManifest.xml
+++ b/permissions/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<application
+ android:name=".PermissionApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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
index 3a34af1..98d08f4 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/Blessing.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/Blessing.java
@@ -4,61 +4,130 @@
package examples.baku.io.permissions;
+import com.google.common.collect.UnmodifiableIterator;
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.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
import java.util.Stack;
-import java.util.UUID;
+
+import examples.baku.io.permissions.util.Utils;
/**
* Created by phamilton on 7/9/16.
*/
-public class Blessing implements Iterable<Blessing.Rule> {
+public class Blessing implements Iterable<Blessing.Permission>, ValueEventListener {
private static final String KEY_PERMISSIONS = "_permissions";
- private static final String KEY_RULES = "rules";
+ private static final String KEY_RULES = "rule";
+
+ private PermissionManager permissionManager;
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<>();
+ private Blessing parentBlessing;
+ private final Map<String, Integer> permissions = new HashMap<>();
+ private final PermissionTree permissionTree = new PermissionTree();
- 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);
+ private final Set<OnBlessingUpdatedListener> blessingListeners = new HashSet<>();
+
+
+ public interface OnBlessingUpdatedListener {
+ void onBlessingUpdated(Blessing blessing);
+
+ void onBlessingRemoved(Blessing blessing);
}
- public Blessing(String target, String source, DatabaseReference ref) {
- setRef(ref);
- setId(ref.getKey());
+ private Blessing(PermissionManager permissionManager, String id, String source, String target) {
+ this.permissionManager = permissionManager;
+ if (id == null) {
+// setRef(permissionManager.getBlessingsRef().push());
+ //TEMP: use a combination of source and target for debugging
+ setRef(permissionManager.getBlessingsRef().child(source + "_" + target));
+ id = this.ref.getKey();
+
+ } else {
+ setRef(permissionManager.getBlessingsRef().child(id));
+ }
+ setId(id);
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 static Blessing create(PermissionManager permissionManager, String source, String target) {
+ return get(permissionManager, null, source, target, true);
+ }
+
+ //root blessings have no source blessing and their id is the same as their target
+ public static Blessing createRoot(PermissionManager permissionManager, String target) {
+ return get(permissionManager, target, null, target, true);
+ }
+
+ public static Blessing get(PermissionManager permissionManager, String id, String source, String target, boolean create) {
+ Blessing blessing = permissionManager.getBlessing(source, target);
+ if (blessing == null && create) {
+ blessing = new Blessing(permissionManager, id, source, target);
+ permissionManager.putBlessing(blessing);
+ }
+ return blessing;
+ }
+
+ public static Blessing fromSnapshot(PermissionManager permissionManager, DataSnapshot snapshot) {
+ String id = snapshot.getKey();
+ String target = snapshot.child("target").getValue(String.class);
+ String source = null;
+ if (snapshot.hasChild("source"))
+ source = snapshot.child("source").getValue(String.class);
+ return get(permissionManager, id, source, target, true);
+ }
+
+ public OnBlessingUpdatedListener addListener(OnBlessingUpdatedListener listener) {
+ blessingListeners.add(listener);
+ listener.onBlessingUpdated(this);
+ return listener;
+ }
+
+ public boolean addListeners(Collection<OnBlessingUpdatedListener> listeners) {
+ return this.blessingListeners.addAll(listeners);
+ }
+
+ public boolean removeListener(OnBlessingUpdatedListener listener) {
+ return blessingListeners.remove(listener);
+ }
+
+ public boolean removeListeners(Collection<OnBlessingUpdatedListener> listeners) {
+ return blessingListeners.removeAll(listeners);
+ }
+
+ private final OnBlessingUpdatedListener parentListener = new OnBlessingUpdatedListener() {
+ @Override
+ public void onBlessingUpdated(Blessing blessing) {
+ permissionTree.parentTree = parentBlessing.permissionTree;
+ notifyListeners();
+ }
+
+ @Override
+ public void onBlessingRemoved(Blessing blessing) {
+ //revoke self
+ revoke();
+ }
+ };
+
public boolean isSynched() {
return snapshot != null;
}
@@ -80,9 +149,42 @@
ref.child("id").setValue(id);
}
- public void setSource(String source) {
- this.source = source;
- ref.child("source").setValue(source);
+ private void setSource(String source) {
+ if (this.source == null && source != null) {
+ if (this.id.equals(source)) {
+ throw new IllegalArgumentException("Source can't be equal to id: " + this.id);
+ }
+ this.source = source;
+ ref.child("source").setValue(source);
+ parentBlessing = permissionManager.getBlessing(source);
+ if (parentBlessing == null) { //retrieve, if manager isn't tracking blessing
+ permissionManager.getBlessingsRef().child(source).addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if (dataSnapshot.exists()) {
+ parentBlessing = Blessing.fromSnapshot(permissionManager, dataSnapshot);
+ parentBlessing.addListener(parentListener);
+ } else { //destroy self if source doesn't exist
+ revoke();
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+
+ }
+ });
+ } else {
+ permissionTree.parentTree = parentBlessing.permissionTree;
+ parentBlessing.addListener(parentListener);
+ }
+ }
+ }
+
+ private void notifyListeners() {
+ for (OnBlessingUpdatedListener listener : blessingListeners) {
+ listener.onBlessingUpdated(this);
+ }
}
public void setTarget(String target) {
@@ -90,26 +192,50 @@
ref.child("target").setValue(target);
}
- public void setSnapshot(DataSnapshot snapshot) {
+ private void setSnapshot(DataSnapshot snapshot) {
if (!snapshot.exists()) {
throw new IllegalArgumentException("empty snapshot");
}
this.snapshot = snapshot;
- setRef(snapshot.getRef());
+ if (snapshot.hasChild(KEY_RULES)) {
+ this.permissionTree.setRoot(new Permission(snapshot.child(KEY_RULES), null, 0));
+ } else {
+ this.permissionTree.setRoot(new Permission());
+ }
}
public Blessing setPermissions(String path, int permissions) {
+ this.permissions.put(path, permissions);
getRef(path).setPermission(permissions);
return this;
}
+ public void setPermissions(Map<String, Integer> permissions) {
+ for (Map.Entry<String, Integer> entry : permissions.entrySet()) {
+ setPermissions(entry.getKey(), permissions.get(entry.getValue()));
+ }
+ }
+
public Blessing clearPermissions(String path) {
getRef(path).clearPermission();
+ this.permissions.remove(path);
+ return this;
+ }
+
+ public Blessing revoke() {
+ if (parentBlessing != null) {
+ parentBlessing.removeListener(parentListener);
+ }
+ for (OnBlessingUpdatedListener listener : blessingListeners) {
+ listener.onBlessingRemoved(this);
+ }
+ ref.removeEventListener(this);
+ rulesRef.removeValue();
return this;
}
//delete all permission above path
- public Blessing revoke(String path) {
+ public Blessing revokePermissions(String path) {
if (path != null) {
rulesRef.child(path).removeValue();
} else {
@@ -118,116 +244,264 @@
return this;
}
- public PermissionReference getRef(String path) {
- PermissionReference result = refCache.get(path);
+ private PermissionReference getRef(String path) {
+ return new PermissionReference(rulesRef, path);
+ }
+
+ private void setRef(DatabaseReference ref) {
+ this.ref = ref;
+ this.rulesRef = ref.child(KEY_RULES);
+
+ ref.addValueEventListener(this);
+ }
+
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ if (dataSnapshot.exists()) {
+ setSnapshot(dataSnapshot);
+ notifyListeners();
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+ databaseError.toException().printStackTrace();
+ }
+
+ //return a blessing interface for granting/revoking permissions
+ public Blessing bless(String target) {
+ Blessing result = getBlessing(target);
if (result == null) {
- result = new PermissionReference(rulesRef, path);
- refCache.put(path, result);
+ if (isDescendantOf(target) || target.equals(this.target)) {
+ throw new IllegalArgumentException("Can't bless a target that already exists in the blessing hiearchy.");
+ }
+ result = Blessing.create(permissionManager, getId(), target);
}
return result;
}
- public void setRef(DatabaseReference ref) {
- this.ref = ref;
- this.rulesRef = ref.child(KEY_RULES);
+ public Blessing getBlessing(String target) {
+ return permissionManager.getBlessing(getId(), target);
}
- 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;
+ public boolean isDescendantOf(String target) {
+ return this.target.equals(target) || parentBlessing != null && parentBlessing.isDescendantOf(target);
}
+
@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 Iterator<Permission> iterator() {
+ return isSynched() ? permissionTree.iterator() : null;
}
- public static class Rule {
- private String path;
- private int permissions;
+ public PermissionTree getPermissionTree() {
+ return permissionTree;
+ }
- public Rule() {
+
+ public static class Permission implements Iterable<Permission> {
+ String key;
+ String path;
+ int inherited;
+ int permissions;
+ final Map<String, Permission> children = new HashMap();
+
+
+ public Permission() {
}
- public Rule(String path, int permissions) {
+ public Permission(DataSnapshot snapshot, String path, int inherited) {
this.path = path;
- this.permissions = permissions;
+ if (path != null) {
+ this.key = snapshot.getKey();
+ }
+ this.inherited = inherited;
+ if (snapshot.hasChild(KEY_PERMISSIONS)) {
+ this.permissions |= snapshot.child(KEY_PERMISSIONS).getValue(Integer.class);
+ }
+ for (DataSnapshot child : snapshot.getChildren()) {
+ if (child.getKey().startsWith("_")) { //ignore keys with '_' prefix
+ continue;
+ }
+ String childPath = child.getKey();
+ if (path != null) {
+ childPath = path + "/" + childPath;
+ }
+ children.put(child.getKey(), new Permission(child, childPath, this.permissions | this.inherited));
+ }
}
- public String getPath() {
- return path;
+ public Permission copy() {
+ Permission result = new Permission();
+ result.key = key;
+ result.path = path;
+ result.inherited = inherited;
+ result.permissions = permissions;
+ for (Permission child : children.values()) {
+ result.children.put(child.key, child.copy());
+ }
+ return result;
+ }
+
+ public void addPermissions(int permission) {
+ this.permissions |= permission;
+ for (Permission child : children.values()) {
+ child.setInherited(getPermissions());
+ }
+ }
+
+ public void setInherited(int permission) {
+ if (this.inherited != permission) {
+ this.inherited = permission;
+ propagateInherited();
+ }
+ }
+
+ public void propagateInherited() {
+ for (Permission child : children.values()) {
+ child.setInherited(getPermissions());
+ }
+ }
+
+ public void removePermissions(int permission) {
+ this.permissions &= ~(permission);
+ propagateInherited();
+ }
+
+ public void checkPermissions(int reference) {
+ this.permissions &= reference;
+ propagateInherited();
+ }
+
+ public void checkPermissions(PermissionTree ref) {
+ for (Permission permission : this) {
+ permission.permissions &= ref.getPermissions(permission.path);
+ }
+ setInherited(inherited & ref.getPermissions(path));
+ }
+
+
+ public Permission child(String path) {
+ int index = path.indexOf("/");
+ if (index != -1) {
+ return this.children.get(path);
+ }
+ Permission child = this.children.get(path.substring(0, index));
+ if (child != null && path.length() - index > 1) {
+ return child.child(path.substring(index));
+ }
+ return null;
}
public int getPermissions() {
- return permissions;
+ return permissions | inherited;
+ }
+
+
+ @Override
+ public Iterator<Permission> iterator() {
+ final Stack<Permission> nodeStack = new Stack<>();
+ nodeStack.push(this);
+
+ final Stack<String> pathStack = new Stack<>();
+ pathStack.push(null); //default rule
+
+ return new UnmodifiableIterator<Permission>() {
+ @Override
+ public boolean hasNext() {
+ return !nodeStack.isEmpty();
+ }
+
+ @Override
+ public Permission next() {
+ Permission node = nodeStack.pop();
+ for (final Permission child : node.children.values()) {
+ nodeStack.push(child);
+ }
+ return node;
+ }
+ };
+ }
+ }
+
+
+ public static class PermissionTree implements Iterable<Permission> {
+ Permission root;
+ final Map<String, Permission> rules = new HashMap<>();
+ PermissionTree parentTree;
+
+ public PermissionTree(DataSnapshot snapshot) {
+ setRoot(new Permission(snapshot, null, 0));
+ }
+
+ public PermissionTree() {
+ setRoot(new Permission());
+ }
+
+ public void setRoot(Permission root) {
+ this.root = root;
+ updateRules();
+ }
+
+ public void merge(PermissionTree tree) {
+ Permission permissionA;
+ Permission permissionB = tree.root;
+ permissionB.checkPermissions(tree); //check no permissions exceed parent
+ Queue<Permission> permissionQueue = new LinkedList<>();
+ permissionQueue.add(permissionB);
+
+ while (!permissionQueue.isEmpty()) {
+ permissionB = permissionQueue.remove();
+ permissionA = rules.get(permissionB.path);
+ permissionA.addPermissions(tree.getPermissions(permissionB.path));
+ for (Permission child : permissionB.children.values()) {
+ if (rules.containsKey(child.path)) {
+ permissionQueue.add(child);
+ } else {
+ Permission childCopy = child.copy();
+ childCopy.setInherited(permissionA.getPermissions());
+ permissionA.children.put(childCopy.key, childCopy);
+ }
+ }
+ }
+ updateRules();
+ }
+
+ private void updateRules() {
+ rules.clear();
+ for (Permission permission : root) {
+ rules.put(permission.path, permission);
+ }
+ }
+
+ public Permission get(String path) {
+ return rules.get(path);
+ }
+
+ public int getPermissions(String path) {
+ path = Utils.getNearestCommonAncestor(path, keySet());
+ Permission permission = get(path);
+ if (permission == null) {
+ return 0;
+ }
+ int result = permission.getPermissions();
+ if (parentTree != null) { //validate
+ result &= parentTree.getPermissions(path);
+ }
+ return result;
+ }
+
+ public Set<String> keySet() {
+ return rules.keySet();
+ }
+
+ public Collection<Permission> values() {
+ return rules.values();
+ }
+
+ @Override
+ public Iterator<Permission> iterator() {
+ return root.iterator();
}
}
}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionApplication.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionApplication.java
new file mode 100644
index 0000000..9240aad
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionApplication.java
@@ -0,0 +1,23 @@
+// 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.Application;
+
+import com.joanzapata.iconify.Iconify;
+import com.joanzapata.iconify.fonts.MaterialModule;
+
+/**
+ * Created by phamilton on 7/20/16.
+ */
+public class PermissionApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ //Add icons
+ Iconify.with(new MaterialModule());
+ }
+}
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
index 851ae81..52b1486 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionManager.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionManager.java
@@ -4,19 +4,23 @@
package examples.baku.io.permissions;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import java.util.Stack;
+
+import examples.baku.io.permissions.util.Utils;
/**
@@ -24,36 +28,40 @@
*/
public class PermissionManager {
- DatabaseReference mDatabaseRef;
- DatabaseReference mBlessingsRef;
- DatabaseReference mRequestsRef;
+ public static final String EXTRA_TIMEOUT = "extraTimeout";
+ public static final String EXTRA_COLOR = "extraColor";
+ private DatabaseReference mDatabaseRef;
+ private DatabaseReference mBlessingsRef;
+ private 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 static final String KEY_ROOT = "root";
+
private String mId;
+ private Blessing rootBlessing;
- final Map<String, PermissionRequest> mRequests = new HashMap<>();
+ //<blessing id, blessing>
+ private final Map<String, Blessing> mBlessings = new HashMap<>();
+ //<source, target, blessing>
+ private final Table<String, String, Blessing> mBlessingsTable = HashBasedTable.create();
+ private final Set<String> mBlessingTargets = new HashSet();
- // final Map<String, Set<OnRequestListener>> requestListeners = new HashMap<>();
- final Set<OnRequestListener> requestListeners = new HashSet<>();
- final Multimap<String, OnReferralListener> referralListeners = HashMultimap.create();
+ private final Map<String, PermissionRequest> mRequests = new HashMap<>();
+ private final Table<String, String, PermissionRequest.Builder> mActiveRequests = HashBasedTable.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<>();
+ private final Multimap<String, OnRequestListener> mRequestListeners = HashMultimap.create(); //<path,, >
+ private final Multimap<String, OnRequestListener> mSubscribedRequests = HashMultimap.create(); //<request id, >
- final Map<String, Integer> mCachedPermissions = new HashMap<>();
- final Multimap<String, OnPermissionChangeListener> mPermissionValueEventListeners = HashMultimap.create();
- final Multimap<String, String> mNearestAncestors = HashMultimap.create();
+ private Blessing.PermissionTree mPermissionTree = new Blessing.PermissionTree();
+ private final Multimap<String, OnPermissionChangeListener> mPermissionValueEventListeners = HashMultimap.create();
+ private final Multimap<String, String> mNearestAncestors = HashMultimap.create();
//TODO: replace string ownerId with Auth
@@ -62,49 +70,44 @@
this.mId = owner;
mRequestsRef = databaseReference.child(KEY_REQUESTS);
- //TODO: only consider requests from sources within the constelattion
+ //TODO: only consider requests from sources within the constellation
mRequestsRef.addChildEventListener(requestListener);
-
mBlessingsRef = mDatabaseRef.child(KEY_BLESSINGS);
- mBlessingsRef.orderByChild("target").equalTo(mId).addChildEventListener(blessingListener);
- mBlessingsRef.orderByChild("source").equalTo(mId).addListenerForSingleValueEvent(grantedBlessingListener);
+
+ this.mId = owner;
+ initRootBlessing();
+ join(mId);
+
}
- 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);
- }
+ public void join(String group) {
+ mBlessingsRef.orderByChild("target").equalTo(group).addChildEventListener(blessingListener);
+ mBlessingTargets.add(group);
+ }
- refreshPermissions();
+ public void leave(String group) {
+ mBlessingsRef.orderByChild("target").equalTo(group).removeEventListener(blessingListener);
+ mBlessingTargets.remove(group);
+ }
+
+ public void initRootBlessing() {
+ rootBlessing = Blessing.createRoot(this, mId);
}
//TODO: optimize this mess. Currently, recalculating entire permission tree.
void refreshPermissions() {
- Map<String, Integer> updatedPermissions = new HashMap<>();
- for (Blessing blessing : mBlessings.values()) {
+ Blessing.PermissionTree updatedPermissionTree = new Blessing.PermissionTree();
+ //received blessings
+ for (Blessing blessing : getReceivedBlessings()) {
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());
- }
- }
+ updatedPermissionTree.merge(blessing.getPermissionTree());
}
}
+ //re-associate listeners with rules
mNearestAncestors.clear();
for (String path : mPermissionValueEventListeners.keySet()) {
- String nearestAncestor = getNearestCommonAncestor(path, updatedPermissions.keySet());
+ String nearestAncestor = Utils.getNearestCommonAncestor(path, updatedPermissionTree.keySet());
if (nearestAncestor != null) {
mNearestAncestors.put(nearestAncestor, path);
}
@@ -112,38 +115,37 @@
Set<String> changedPermissions = new HashSet<>();
- Set<String> removedPermissions = new HashSet<>(mCachedPermissions.keySet());
- removedPermissions.removeAll(updatedPermissions.keySet());
+ //determine removed permissions
+ Sets.SetView<String> removedPermissions = Sets.difference(mPermissionTree.keySet(), updatedPermissionTree.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);
+ String newPath = Utils.getNearestCommonAncestor(path, updatedPermissionTree.keySet());
+ int previous = mPermissionTree.getPermissions(path);
+ int current = updatedPermissionTree.getPermissions(newPath);
+ if (previous != current) {
changedPermissions.add(path);
- } else {
- int previous = mCachedPermissions.get(path);
- if (previous != current) {
- mCachedPermissions.put(path, current);
- changedPermissions.add(path);
- }
}
}
+ //compare previous tree
+ for (Blessing.Permission permission : updatedPermissionTree.values()) {
+ int previous = mPermissionTree.getPermissions(permission.path);
+ int current = updatedPermissionTree.getPermissions(permission.path);
+ if (previous != current) {
+ changedPermissions.add(permission.path);
+ }
+ }
+
+ mPermissionTree = updatedPermissionTree;
+
+ //notify listeners
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);
+ int permission = getPermissions(path);
if (mNearestAncestors.containsKey(path)) {
for (String listenerPath : mNearestAncestors.get(path)) {
if (mPermissionValueEventListeners.containsKey(listenerPath)) {
@@ -155,75 +157,91 @@
}
}
- 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);
- }
+
+ public Set<PermissionRequest> getRequests(String path) {
+ Set<PermissionRequest> result = new HashSet<>();
+ for (PermissionRequest request : mRequests.values()) {
+ if (getAllPaths(request.getPath()).contains(path)) {
+ result.add(request);
}
}
-
- @Override
- public void onCancelled(DatabaseError databaseError) {
-
- }
- };
-
- void onBlessingRemoved(DataSnapshot snapshot) {
- Blessing removedBlessing = mBlessings.remove(snapshot.getKey());
- refreshPermissions();
+ return result;
}
- public Blessing getGrantedBlessing(String target) {
- return mGrantedBlessings.get(target);
+ public PermissionRequest getRequest(String rId) {
+ return mRequests.get(rId);
}
- 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;
+ public Blessing getRootBlessing() {
+ return rootBlessing;
+ }
+
+ public Set<Blessing> getReceivedBlessings() {
+ Set<Blessing> result = new HashSet<>();
+ for (String target : mBlessingTargets) {
+ result.addAll(mBlessingsTable.column(target).values());
+ }
+ return result;
+ }
+
+ public Set<Blessing> getGrantedBlessings(String src) {
+ return new HashSet<>(mBlessingsTable.row(src).values());
+ }
+
+ public Blessing putBlessing(Blessing blessing) {
+ String source = blessing.getSource();
+ String target = blessing.getTarget();
+ mBlessings.put(blessing.getId(), blessing);
+ if (source == null) {
+ source = KEY_ROOT;
+ }
+ return mBlessingsTable.put(source, target, blessing);
+ }
+
+ public Blessing getBlessing(String id) {
+ return mBlessings.get(id);
+ }
+
+ public Blessing getBlessing(String source, String target) {
+ if (source == null) {
+ source = KEY_ROOT;
+ }
+ return mBlessingsTable.get(source, target);
+ }
+
+ public void removeBlessing(String rId) {
+ Blessing removedBlessing = mBlessings.remove(rId);
+ if (removedBlessing != null) {
+ mBlessingsTable.remove(removedBlessing.getSource(), removedBlessing.getTarget());
+
+ }
}
//return a blessing interface for granting/revoking permissions
+ //uses local device blessing as root
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;
+ return rootBlessing.bless(target);
+ }
+
+ public DatabaseReference getBlessingsRef() {
+ return mBlessingsRef;
}
private ChildEventListener requestListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
- onBlessingUpdated(dataSnapshot);
+ onRequestUpdated(dataSnapshot);
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
- onBlessingUpdated(dataSnapshot);
+ onRequestUpdated(dataSnapshot);
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
- onBlessingRemoved(dataSnapshot);
+ onRequestRemoved(dataSnapshot);
}
@Override
@@ -237,45 +255,109 @@
}
};
+ public void finishRequest(String rId) {
+ //TODO: notify source entity and ignore instead of removing
+ mRequestsRef.child(rId).removeValue();
+ }
+
+ public void grantRequest(PermissionRequest request) {
+ Blessing blessing = bless(request.getSource());
+ blessing.setPermissions(request.getPath(), request.getPermissions());
+ finishRequest(request.getId());
+ }
+
private void onRequestUpdated(DataSnapshot snapshot) {
- if (!snapshot.exists()) return;
+ 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);
+ if (request == null) {
+ return;
+ }
+
+ String requestPath = request.getPath();
+ if (requestPath == null) {
+ return;
+ }
+
+ //ignore local requests
+ if (mId.equals(request.getSource())) {
+ return;
+ }
+
+ //Check if request permissions can be granted by this instance
+ if ((getPermissions(requestPath) & request.getPermissions()) != request.getPermissions()) {
+ return;
+ }
+
+ String rId = request.getId();
+ String source = request.getSource();
+ mRequests.put(rId, request);
+
+ if (mSubscribedRequests.containsKey(rId)) {
+ for (OnRequestListener listener : new HashSet<>(mSubscribedRequests.get(rId))) {
+ if (!listener.onRequest(request, bless(source))) {
+ //cancel subscription
+ mSubscribedRequests.remove(rId, listener);
+ }
+ }
+ } else {
+ for (String path : getAllPaths(request.getPath())) {
+ for (OnRequestListener listener : mRequestListeners.get(path)) {
+ if (listener.onRequest(request, bless(source))) {
+ //add subscription
+ mSubscribedRequests.put(request.getId(), listener);
+ }
+ }
}
}
}
- //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);
+ String source = request.getSource();
+ if (request != null && !mId.equals(source)) { //ignore local requests
+ for (OnRequestListener listener : mSubscribedRequests.removeAll(request.getId())) {
+ listener.onRequestRemoved(request, bless(source));
}
}
}
+ //allows
+ private Set<String> getAllPaths(String path) {
+ Set<String> result = new HashSet<>();
+ result.add(path);
+ result.add("*");
+ String subpath = path;
+ int index;
+ while ((index = subpath.lastIndexOf("/")) != -1) {
+ subpath = subpath.substring(0, index);
+ result.add(subpath + "/*");
+ }
+ return result;
+ }
private ChildEventListener blessingListener = new ChildEventListener() {
@Override
- public void onChildAdded(DataSnapshot dataSnapshot, String s) {
- onBlessingUpdated(dataSnapshot);
+ public void onChildAdded(DataSnapshot snapshot, String s) {
+ Blessing receivedBlessing = Blessing.fromSnapshot(PermissionManager.this, snapshot);
+ receivedBlessing.addListener(blessingChangedListner);
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
- onBlessingUpdated(dataSnapshot);
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
- onBlessingRemoved(dataSnapshot);
+ Blessing removedBlessing = mBlessings.remove(dataSnapshot.getKey());
+ if (removedBlessing != null) {
+ removedBlessing.removeListener(blessingChangedListner);
+ mBlessingsTable.remove(removedBlessing.getSource(), removedBlessing.getTarget());
+ refreshPermissions();
+ }
}
@Override
@@ -289,29 +371,30 @@
}
};
- 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);
+ private Blessing.OnBlessingUpdatedListener blessingChangedListner = new Blessing.OnBlessingUpdatedListener() {
+ @Override
+ public void onBlessingUpdated(Blessing blessing) {
+ refreshPermissions();
}
- return current;
+
+ @Override
+ public void onBlessingRemoved(Blessing blessing) {
+ refreshPermissions();
+ }
+ };
+
+
+ public int getPermissions(String path) {
+ return mPermissionTree.getPermissions(path);
}
public OnPermissionChangeListener addPermissionEventListener(String path, OnPermissionChangeListener listener) {
int current = FLAG_DEFAULT;
mPermissionValueEventListeners.put(path, listener);
- String nearestAncestor = getNearestCommonAncestor(path, mCachedPermissions.keySet());
+ String nearestAncestor = Utils.getNearestCommonAncestor(path, mPermissionTree.keySet());
if (nearestAncestor != null) {
- current = getPermission(nearestAncestor);
+ current = getPermissions(nearestAncestor);
mNearestAncestors.put(nearestAncestor, path);
}
listener.onPermissionChange(current);
@@ -320,50 +403,83 @@
public void removePermissionEventListener(String path, OnPermissionChangeListener listener) {
mPermissionValueEventListeners.remove(path, listener);
-
- String nca = getNearestCommonAncestor(path, mCachedPermissions.keySet());
+ String nca = Utils.getNearestCommonAncestor(path, mPermissionTree.keySet());
mNearestAncestors.remove(nca, path);
}
- public void removeOnRequestListener(PermissionManager.OnRequestListener requestListener) {
- requestListeners.remove(requestListener);
+
+ public void removeOnRequestListener(String path, OnRequestListener requestListener) {
+ mRequestListeners.remove(path, requestListener);
+ if (mRequestListeners.values().contains(requestListener)) {
+ //TODO: this doesn't catch cases where one request listener unsubscribed
+ for (Map.Entry<String, OnRequestListener> entry : mSubscribedRequests.entries()) {
+ if (entry.getValue().equals(requestListener)) {
+ String rId = entry.getKey();
+ PermissionRequest request = mRequests.get(rId);
+ if (getAllPaths(request.getPath()).contains(path)) {
+ mSubscribedRequests.remove(rId, requestListener);
+ }
+ }
+ }
+
+ } else {
+ mSubscribedRequests.values().remove(requestListener);
+ }
}
- public PermissionManager.OnRequestListener addOnRequestListener(PermissionManager.OnRequestListener requestListener) {
- requestListeners.add(requestListener);
+ public OnRequestListener addOnRequestListener(String path, OnRequestListener requestListener) {
+ mRequestListeners.put(path, requestListener);
+ for (Map.Entry<String, PermissionRequest> entry : mRequests.entrySet()) {
+ String rId = entry.getKey();
+ PermissionRequest request = entry.getValue();
+ Set<String> requestPaths = getAllPaths(request.getPath());
+ if (requestPaths.contains(path)) {
+ String source = request.getSource();
+ if (requestListener.onRequest(request, bless(source))) {
+ mSubscribedRequests.put(rId, requestListener);
+ }
+ }
+ }
return requestListener;
}
- public void removeOnReferralListener(String path, OnReferralListener referralListener) {
- referralListeners.remove(path, referralListener);
+ public PermissionRequest.Builder request(String path, String group) {
+ PermissionRequest.Builder builder = mActiveRequests.get(group, path);
+ if (builder == null) {
+ builder = new PermissionRequest.Builder(mRequestsRef.push(), path, mId);
+ mActiveRequests.put(group, path, builder);
+ }
+ return builder;
}
- public OnReferralListener addOnReferralListener(String path, OnReferralListener referralListener) {
- referralListeners.put(path, referralListener);
- return referralListener;
+ public void cancelRequests(String group) {
+ Collection<PermissionRequest.Builder> builders = mActiveRequests.row(group).values();
+ for (PermissionRequest.Builder builder : builders) {
+ builder.cancel();
+ }
+ builders.clear();
}
- public void refer(PermissionReferral referral) {
+ public void cancelRequest(String group, String path) {
+ PermissionRequest.Builder builder = mActiveRequests.remove(group, path);
+ if (builder != null) {
+ builder.cancel();
+ }
}
- 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 void onDestroy() {
+ mBlessingsRef.removeEventListener(blessingListener);
+ mRequestsRef.removeEventListener(requestListener);
+ for (Blessing blessing : new HashSet<Blessing>(mBlessings.values())) {
+ blessing.revoke();
+ }
}
public interface OnRequestListener {
- boolean onRequest(PermissionRequest request);
+ boolean onRequest(PermissionRequest request, Blessing blessing);
- void onRequestRemoved(PermissionRequest request);
- }
-
- public interface OnReferralListener {
- void onReferral();
+ void onRequestRemoved(PermissionRequest request, Blessing blessing);
}
public interface OnPermissionChangeListener {
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
deleted file mode 100644
index ac10f33..0000000
--- a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionReferral.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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
index e856113..5437400 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionRequest.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionRequest.java
@@ -4,7 +4,9 @@
package examples.baku.io.permissions;
-import java.security.Permission;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.ServerValue;
+
import java.util.HashMap;
import java.util.Map;
@@ -15,12 +17,18 @@
//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 static final String EXTRA_TITLE = "title";
- public PermissionRequest(){}
+ private String id;
+ private String path;
+ private String source;
+ private int permissions;
+ private int flags;
+ private Map<String, String> extras = new HashMap<>();
+ private long timeStamp;
+
+ public PermissionRequest() {
+ }
public String getSource() {
return source;
@@ -30,20 +38,13 @@
this.source = source;
}
- public Map<String, Integer> getPermissions() {
- return permissions;
+
+ public Map<String, String> getExtras() {
+ return extras;
}
- 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 void setExtras(Map<String, String> extras) {
+ this.extras = extras;
}
public String getId() {
@@ -54,12 +55,89 @@
this.id = id;
}
- public static class Builder{
- private PermissionRequest request;
+ public String getPath() {
+ return path;
+ }
- public Builder(String path){
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public int getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(int permissions) {
+ this.permissions = permissions;
+ }
+
+ public int getFlags() {
+ return flags;
+ }
+
+ public void setFlags(int flags) {
+ this.flags = flags;
+ }
+
+ public Map<String, String> getTimeStamp() {
+ return ServerValue.TIMESTAMP;
+ }
+
+ public void setTimeStamp(long timeStamp) {
+ this.timeStamp = timeStamp;
+ }
+
+ //accept suggested permissions
+ public void grant(PermissionManager manager) {
+ manager.grantRequest(this);
+ }
+
+ public void finish(PermissionManager manager) {
+ manager.finishRequest(id);
+ }
+
+ public static class Builder {
+ private PermissionRequest request;
+ private DatabaseReference ref;
+
+ public Builder(DatabaseReference ref, String path, String source) {
+ this.ref = ref;
this.request = new PermissionRequest();
+ request.setId(ref.getKey());
+ request.setPath(path);
+ request.setSource(source);
}
+
+ public PermissionRequest.Builder putExtra(String key, String value) {
+ this.request.extras.put(key, value);
+ return this;
+ }
+
+
+ public PermissionRequest.Builder setPermissions(int suggested) {
+ request.setPermissions(suggested);
+ return this;
+ }
+
+ public int getFlags() {
+ return request.getFlags();
+ }
+
+ public PermissionRequest.Builder setFlags(int flags) {
+ this.request.flags = flags;
+ return this;
+ }
+
+ public void cancel() {
+ this.ref.removeValue();
+ }
+
+ public PermissionRequest udpate() {
+ //TODO: check valid
+ this.ref.setValue(request);
+ return request;
+ }
+
}
}
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
index 756693c..83a544a 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionService.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionService.java
@@ -11,10 +11,12 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.graphics.drawable.Icon;
import android.os.Binder;
import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
+import android.widget.Toast;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
@@ -23,6 +25,8 @@
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
+import com.joanzapata.iconify.IconDrawable;
+import com.joanzapata.iconify.fonts.MaterialIcons;
import org.json.JSONException;
import org.json.JSONObject;
@@ -38,6 +42,7 @@
import examples.baku.io.permissions.examples.EmailActivity;
import examples.baku.io.permissions.messenger.Messenger;
import examples.baku.io.permissions.messenger.Message;
+import examples.baku.io.permissions.util.Utils;
public class PermissionService extends Service {
@@ -53,22 +58,25 @@
return mRunning;
}
- static final int FOREGROUND_NOTIFICATION_ID = 3278;
- static final int FOCUS_NOTIFICATION = 43254;
+ static final int FOREGROUND_NOTIFICATION_ID = -3278;
+ static final int FOCUS_NOTIFICATION = -43254;
static final String KEY_BLESSINGS = PermissionManager.KEY_BLESSINGS;
+ public static final String EXTRA_COMMAND = "type";
+ public static final String EXTRA_REQUEST_ID = "requestId";
+ public static final String EXTRA_NOTIFICATION_ID = "notificationId";
+ public static final String EXTRA_ACTION_ID = "notificationId";
+
NotificationManager mNotificationManager;
FirebaseDatabase mFirebaseDB;
DatabaseReference mDevicesReference;
- DatabaseReference mRequestsReference;
DatabaseReference mMessengerReference;
Messenger mMessenger;
- DatabaseReference mPermissionsReference;
PermissionManager mPermissionManager;
Blessing mDeviceBlessing;
@@ -76,19 +84,31 @@
private String mDeviceId;
- private IBinder mBinder = new PermissionServiceBinder();
-
+ private int mNotificationCounter = 0;
+ private int mActionCounter = 0;
private String mFocus;
private Map<String, DeviceData> mDiscovered = new HashMap<>();
+ private HashSet<DiscoveryListener> mDiscoveryListener = new HashSet<>();
private Map<String, Integer> mDiscoveredNotifications = new HashMap<>();
- private HashSet<DiscoveryListener> mDiscoveryListener = new HashSet<>();
+ private Map<String, Integer> mRequestNotifications = new HashMap<>();
- public void revokeAll() {
- for (Blessing blessing : mPermissionManager.mGrantedBlessings.values()) {
- blessing.revoke(null);
- }
+
+ private Map<String, ActionCallback> mActionListeners = new HashMap<>();
+
+
+ private Icon shareIcon;
+ private Icon zoomIcon;
+ private Icon closeIcon;
+ private Icon keyIcon;
+ private Icon grantIcon;
+ private Icon deviceIcon;
+ private Icon castIcon;
+
+
+ public interface ActionCallback {
+ void onAction(Intent intent);
}
public interface DiscoveryListener {
@@ -117,20 +137,68 @@
mFirebaseDB = FirebaseDatabase.getInstance();
mDevicesReference = mFirebaseDB.getReference("_devices");
- mRequestsReference = mFirebaseDB.getReference("requests");
- mPermissionsReference = mFirebaseDB.getReference("permissions");
+
+ shareIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_share));
+ zoomIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_zoom_in));
+ closeIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_close));
+ keyIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_vpn_key));
+ grantIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_check));
+ deviceIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_phone_android));
+ castIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_cast));
+
mPermissionManager = new PermissionManager(mFirebaseDB.getReference(), mDeviceId);
- mPermissionManager.addOnRequestListener(new PermissionManager.OnRequestListener() {
+ mPermissionManager.getRootBlessing().setPermissions("documents/" + mDeviceId, PermissionManager.FLAG_READ | PermissionManager.FLAG_WRITE);
+
+ mPermissionManager.join("public");
+
+ mPermissionManager.addOnRequestListener("*", new PermissionManager.OnRequestListener() {
@Override
- public boolean onRequest(PermissionRequest request) {
+ public boolean onRequest(PermissionRequest request, Blessing blessing) {
+
+ int nId = mNotificationCounter++;
+ String sourceName = "unknown device";
+ String source = request.getSource();
+ if (source != null && mDiscovered.containsKey(source)) {
+ sourceName = mDiscovered.get(source).getName();
+ }
+
+ Intent acceptRequestIntent = new Intent(PermissionService.this, PermissionService.class);
+ acceptRequestIntent.putExtra(EXTRA_COMMAND, "acceptRequest");
+ acceptRequestIntent.putExtra(EXTRA_REQUEST_ID, request.getId());
+ acceptRequestIntent.putExtra(EXTRA_NOTIFICATION_ID, nId);
+ PendingIntent acceptRequestPendingIntent = PendingIntent.getService(PermissionService.this, mActionCounter++, acceptRequestIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Intent rejectRequestIntent = new Intent(PermissionService.this, PermissionService.class);
+ rejectRequestIntent.putExtra(EXTRA_COMMAND, "rejectRequest");
+ rejectRequestIntent.putExtra(EXTRA_REQUEST_ID, request.getId());
+ rejectRequestIntent.putExtra(EXTRA_NOTIFICATION_ID, nId);
+ PendingIntent rejectRequestPendingIntent = PendingIntent.getService(PermissionService.this, mActionCounter++, rejectRequestIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ mNotificationManager.cancel(nId);
+
+
+ Notification notification = new Notification.Builder(PermissionService.this)
+ .setSmallIcon(keyIcon)
+ .setContentTitle("Permission request from " + sourceName)
+ .addAction(new Notification.Action.Builder(grantIcon, "Grant", acceptRequestPendingIntent).build())
+// .addAction(new Notification.Action.Builder(R.drawable.ic_close_black_24dp, "Reject", rejectRequestPendingIntent).build())
+ .setDeleteIntent(rejectRequestPendingIntent)
+ .setVibrate(new long[]{100})
+ .setPriority(Notification.PRIORITY_MAX)
+ .build();
+ mNotificationManager.notify(nId, notification);
+ mRequestNotifications.put(request.getId(), nId);
+
return true;
}
@Override
- public void onRequestRemoved(PermissionRequest request) {
-
+ public void onRequestRemoved(PermissionRequest request, Blessing blessing) {
+ if (mRequestNotifications.containsKey(request.getId())) {
+ mNotificationManager.cancel(mRequestNotifications.get(request.getId()));
+ }
}
});
@@ -138,7 +206,6 @@
initForegroundNotification();
registerDevice();
- initDeviceBlessing();
initMessenger();
initDiscovery();
@@ -154,44 +221,67 @@
}
- public void setFocus(String dId) {
+ public void addToConstellation(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);
+ PendingIntent dismissPending = PendingIntent.getService(this, mActionCounter++, dismissIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification.Builder notificationBuilder = new Notification.Builder(this)
.setContentTitle(title)
.setContentText(subtitle)
- .setSmallIcon(icon)
+ .setSmallIcon(deviceIcon)
.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());
+ //add contextual actions
+ if (mLocalDevice != null && mLocalDevice.getStatus().containsKey(ComposeActivity.EXTRA_MESSAGE_PATH)) {
+ final String localPath = mLocalDevice.getStatus().get(ComposeActivity.EXTRA_MESSAGE_PATH);
+ final String focus = device.getId();
+ notificationBuilder.addAction(createActionCallback(castIcon, "Cast Message", "castMessage", new ActionCallback() {
+ @Override
+ public void onAction(Intent intent) {
+ try {
+ JSONObject castArgs = new JSONObject();
+ castArgs.put("activity", ComposeActivity.class.getSimpleName());
+ castArgs.put(ComposeActivity.EXTRA_MESSAGE_PATH, localPath);
+ mMessenger.to(focus).emit("cast", castArgs.toString());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }));
+ }
+ //add contextual actions
+ if (device != null && device.getStatus().containsKey(ComposeActivity.EXTRA_MESSAGE_PATH)) {
+ String focusPath = device.getStatus().get(ComposeActivity.EXTRA_MESSAGE_PATH);
+ Intent emailIntent = new Intent(PermissionService.this, ComposeActivity.class);
+ emailIntent.putExtra(ComposeActivity.EXTRA_MESSAGE_PATH, focusPath);
+ emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ notificationBuilder.addAction(new Notification.Action.Builder(castIcon, "Pull Message", PendingIntent.getActivity(this, 0, emailIntent, PendingIntent.FLAG_CANCEL_CURRENT)).build());
}
Notification notification = notificationBuilder.build();
mNotificationManager.notify(FOCUS_NOTIFICATION, notification);
}
+ private Notification.Action createActionCallback(Icon icon, String title, String actionId, ActionCallback callback) {
+ mActionListeners.put(actionId, callback);
+ Intent actionIntent = new Intent(this, PermissionService.class);
+ actionIntent.putExtra(EXTRA_COMMAND, "actionCallback");
+ actionIntent.putExtra(EXTRA_ACTION_ID, actionId);
+ PendingIntent actionPendingIntent = PendingIntent.getService(this, mActionCounter++, actionIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ return new Notification.Action.Builder(icon, title, actionPendingIntent).build();
+ }
+
public PermissionManager getPermissionManager() {
return mPermissionManager;
@@ -206,7 +296,11 @@
}
public void setStatus(String key, String value) {
- mDevicesReference.child(mDeviceId).child("status").child(key).child(value);
+ mDevicesReference.child(mDeviceId).child("status").child(key).setValue(value);
+ }
+
+ public void clearStatus(String key) {
+ mDevicesReference.child(mDeviceId).child("status").child(key).removeValue();
}
public FirebaseDatabase getFirebaseDB() {
@@ -221,22 +315,22 @@
void initForegroundNotification() {
Intent contentIntent = new Intent(this, PermissionService.class);
- PendingIntent contentPendingIntent = PendingIntent.getActivity(this, 0, contentIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent contentPendingIntent = PendingIntent.getActivity(this, mActionCounter++, 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);
+ discoverIntent.putExtra(EXTRA_COMMAND, "discover");
+ PendingIntent discoverPendingIntent = PendingIntent.getService(this, mActionCounter++, discoverIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent closeIntent = new Intent(getApplicationContext(), PermissionService.class);
- closeIntent.putExtra("type", "close");
- PendingIntent closePendingIntent = PendingIntent.getService(this, 2, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ closeIntent.putExtra(EXTRA_COMMAND, "close");
+ PendingIntent closePendingIntent = PendingIntent.getService(this, mActionCounter++, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification.Builder(this)
.setContentIntent(contentPendingIntent)
- .setSmallIcon(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())
+ .setSmallIcon(shareIcon)
+ .setContentTitle("Discovery service running")
+ .addAction(new Notification.Action.Builder(zoomIcon, "Discover", discoverPendingIntent).build())
+ .addAction(new Notification.Action.Builder(closeIcon, "Stop", closePendingIntent).build())
.build();
startForeground(FOREGROUND_NOTIFICATION_ID, notification);
}
@@ -245,32 +339,8 @@
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 Blessing getRootBlessing() {
+ return mPermissionManager.getRootBlessing();
}
public void initMessenger() {
@@ -310,15 +380,18 @@
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent != null && intent.hasExtra("type")) {
- String type = intent.getStringExtra("type");
+
+ //TODO: move to a broadcast receiver. StartService intents are not ideal.
+ if (intent != null && intent.hasExtra(EXTRA_COMMAND)) {
+ String type = intent.getStringExtra(EXTRA_COMMAND);
l("start command " + type);
- //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);
+ if ("actionCallback".equals(type)) {
+ if (intent.hasExtra(EXTRA_ACTION_ID)) {
+ String aId = intent.getStringExtra(EXTRA_ACTION_ID);
+ if (mActionListeners.containsKey(aId)) {
+ mActionListeners.get(aId).onAction(intent);
+ }
}
} else if ("discover".equals(type)) {
@@ -328,12 +401,36 @@
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 ("acceptRequest".equals(type)) {
+ if (intent.hasExtra(EXTRA_REQUEST_ID)) {
+ String rId = intent.getStringExtra(EXTRA_REQUEST_ID);
+ PermissionRequest request = mPermissionManager.getRequest(rId);
+ if (request != null) {
+ mPermissionManager.grantRequest(request);
+ } else {
+ Toast.makeText(getApplicationContext(), "Expired request", 0).show();
+ }
+ }
+ if (intent.hasExtra(EXTRA_NOTIFICATION_ID)) {
+ mNotificationManager.cancel(intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1));
+ }
+ } else if ("rejectRequest".equals(type)) {
+ if (intent.hasExtra(EXTRA_REQUEST_ID)) {
+ String rId = intent.getStringExtra(EXTRA_REQUEST_ID);
+ mPermissionManager.finishRequest(rId);
+ }
+ } else if ("dismiss".equals(type)) {
+ String dId = intent.getStringExtra("deviceId");
+ if (dId != null) {
+ //revoke all blessings
+ for (Blessing blessing : mPermissionManager.getReceivedBlessings()) {
+ Blessing granted = blessing.getBlessing(dId);
+ if (granted != null) {
+ granted.revoke();
+ }
+ }
+ }
} else if ("close".equals(type)) {
stopSelf();
@@ -351,21 +448,21 @@
Intent discoverIntent = new Intent(this, PermissionService.class);
discoverIntent.putExtra("type", "discover");
- PendingIntent discoverPendingIntent = PendingIntent.getService(this, 0, discoverIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent discoverPendingIntent = PendingIntent.getService(this, mActionCounter++, 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);
+ PendingIntent castPendingIntent = PendingIntent.getService(this, mActionCounter++, 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)
+ .setContentIntent(PendingIntent.getActivity(this, mActionCounter++, contentIntent, 0))
+ .setSmallIcon(keyIcon)
.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())
+ .addAction(new Notification.Action.Builder(castIcon, "Cast", castPendingIntent).build())
+ .addAction(new Notification.Action.Builder(zoomIcon, "Discover", discoverPendingIntent).build())
.build();
refreshForegroundNotification(notification);
@@ -471,6 +568,15 @@
}
+ @Override
+ public void onDestroy() {
+ //TODO: clean up firebase listeners in permission manager.
+ if (mPermissionManager != null) {
+ mPermissionManager.onDestroy();
+ }
+ super.onDestroy();
+ }
+
public static void start(Context context) {
context.startService(new Intent(context, PermissionService.class));
}
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
index 0ace780..7d6e159 100644
--- 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
@@ -16,7 +16,6 @@
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 {
@@ -72,7 +71,7 @@
}
setResult(0, result);
}else{
- mPermissionService.setFocus(dId);
+ mPermissionService.addToConstellation(dId);
}
finish();
return true;
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
index a8b23cc..250e62b 100644
--- 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
@@ -22,22 +22,23 @@
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.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
-import com.google.firebase.database.ValueEventListener;
+import com.joanzapata.iconify.IconDrawable;
+import com.joanzapata.iconify.fonts.MaterialIcons;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
-import java.util.Map;
import java.util.UUID;
+import examples.baku.io.permissions.Blessing;
import examples.baku.io.permissions.PermissionManager;
-import examples.baku.io.permissions.discovery.DeviceData;
+import examples.baku.io.permissions.PermissionRequest;
import examples.baku.io.permissions.PermissionService;
import examples.baku.io.permissions.R;
import examples.baku.io.permissions.discovery.DevicePickerActivity;
@@ -54,11 +55,12 @@
private String mDeviceId;
private String mId;
private PermissionService mPermissionService;
+ private PermissionManager mPermissionManager;
private DatabaseReference mMessageRef;
private DatabaseReference mSyncedMessageRef;
- String sourceId;
-
+ private Blessing mCastBlessing;
+ private Blessing mPublicBlessing;
EditText mToText;
EditText mFrom;
@@ -70,16 +72,11 @@
TextInputLayout mSubjectLayout;
TextInputLayout mMessageLayout;
-
- Map<String, Integer> permissions = new HashMap<>();
-
+ Multimap<String, PermissionRequest> mRequests = HashMultimap.create();
+ HashMap<String, TextInputLayout> mEditContainers = new HashMap<>();
+ HashMap<String, Integer> mPermissions = 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) {
@@ -87,6 +84,8 @@
setContentView(R.layout.activity_compose);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
toolbar.setTitle("Compose Message");
@@ -119,6 +118,10 @@
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);
+ menu.findItem(R.id.action_cast).setIcon(
+ new IconDrawable(this, MaterialIcons.md_cast)
+ .color(Color.WHITE)
+ .actionBarSize());
return true;
}
@@ -134,14 +137,16 @@
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);
+ 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) {
+ } else if (id == android.R.id.home) {
+ finish();
}
return super.onOptionsItemSelected(item);
@@ -149,9 +154,10 @@
void sendMessage() {
-
//TODO: PermissionManager.request()
-
+ mPermissionManager.request(mPath + "/send", mDeviceId)
+ .putExtra(PermissionManager.EXTRA_TIMEOUT, "2000")
+ .putExtra(PermissionManager.EXTRA_COLOR, "#F00");
finish();
}
@@ -160,16 +166,28 @@
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);
+ String targetDevice = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ID);
+
+ if (!mOwner.equals(targetDevice)) {
+ //find most appropriate blessing to extend from
+ mCastBlessing = mPermissionManager.getBlessing(mOwner, mDeviceId);
+ if (mCastBlessing == null) {
+ mCastBlessing = mPermissionManager.getRootBlessing();
+ }
+ mCastBlessing.bless(targetDevice)
+ .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());
+ mPermissionService.getMessenger().to(targetDevice).emit("cast", castArgs.toString());
+
+ mPermissionService.addToConstellation(targetDevice);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -184,6 +202,7 @@
if (mPermissionService != null) {
+ mPermissionManager = mPermissionService.getPermissionManager();
mDeviceId = mPermissionService.getDeviceId();
Intent intent = getIntent();
@@ -207,14 +226,36 @@
mPath = "documents/" + mDeviceId + "/emails/messages/" + mId;
}
+ //parse path to get owner
+ String[] pathElements = mPath.split("/");
+ if (pathElements != null && pathElements.length > 1) {
+ mOwner = pathElements[1];
+ }
+
mMessageRef = mPermissionService.getFirebaseDB().getReference(mPath);
mSyncedMessageRef = mMessageRef.child("syncedValues");
- mPermissionService.getPermissionManager().addPermissionEventListener(mPath, messagePermissionListener);
+ mPermissionManager.addPermissionEventListener(mPath, messagePermissionListener);
wrapTextField(mToLayout, "to");
wrapTextField(mFromLayout, "from");
wrapTextField(mSubjectLayout, "subject");
wrapTextField(mMessageLayout, "message");
+ mPublicBlessing = mPermissionManager.bless("public")
+ .setPermissions(mPath + "/subject", PermissionManager.FLAG_READ);
+
+ mPermissionManager.addOnRequestListener("documents/" + mDeviceId + "/emails/messages/" + mId + "/*", new PermissionManager.OnRequestListener() {
+ @Override
+ public boolean onRequest(PermissionRequest request, Blessing blessing) {
+ mRequests.put(request.getPath(), request);
+ return true;
+ }
+
+ @Override
+ public void onRequestRemoved(PermissionRequest request, Blessing blessing) {
+
+ }
+ });
+
mPermissionService.setStatus(EXTRA_MESSAGE_PATH, mPath);
}
@@ -240,31 +281,52 @@
}
- void wrapTextField(final TextInputLayout editContainer, final String key) {
+ void updateTextField(final String key) {
+ String path = "documents/" + mDeviceId + "/emails/messages/" + mId + "/" + key;
+ Integer current = mPermissions.get(path);
+ if (current == null)
+ current = 0;
+
+ TextInputLayout editContainer = mEditContainers.get(key);
final EditText edit = editContainer.getEditText();
- mPermissionService.getPermissionManager().addPermissionEventListener(mPath + "/" + key, new PermissionManager.OnPermissionChangeListener() {
+ if ((current & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
+ edit.setEnabled(true);
+ editContainer.setOnClickListener(null);
+ edit.setFocusable(true);
+ edit.setBackgroundColor(Color.TRANSPARENT);
+ linkTextField(edit, key);
+ } else if ((current & PermissionManager.FLAG_READ) == PermissionManager.FLAG_READ) {
+ edit.setEnabled(false);
+ editContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mPermissionManager.request(mPath + "/" + key, mDeviceId + mId)
+ .setPermissions(PermissionManager.FLAG_WRITE)
+ .udpate();
+ }
+ });
+ edit.setFocusable(true);
+ edit.setBackgroundColor(Color.TRANSPARENT);
+ linkTextField(edit, key);
+ } else {
+ unlinkTextField(key);
+ edit.setEnabled(false);
+ editContainer.setOnClickListener(null);
+ edit.setFocusable(false);
+ edit.setBackgroundColor(Color.BLACK);
+ }
+ }
+
+ void wrapTextField(final TextInputLayout editContainer, final String key) {
+ mEditContainers.put(key, editContainer);
+ final String path = "documents/" + mDeviceId + "/emails/messages/" + mId + "/" + key;
+
+ mPermissionManager.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);
- }
+ mPermissions.put(path, current);
+ updateTextField(key);
}
@Override
@@ -326,7 +388,12 @@
unlinkTextField("subject");
unlinkTextField("message");
if (mPermissionService != null) {
- mPermissionService.revokeAll();
+ if (mPublicBlessing != null) {
+ mPublicBlessing.revokePermissions(mPath);
+ }
+
+ //cancel all requests made from this activity
+ mPermissionManager.cancelRequests(mDeviceId + mId);
}
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
index db2a7f4..c42623f 100644
--- 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
@@ -7,9 +7,9 @@
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.provider.ContactsContract;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.CardView;
@@ -23,7 +23,6 @@
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;
@@ -34,15 +33,17 @@
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
+import com.joanzapata.iconify.widget.IconTextView;
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.Blessing;
+import examples.baku.io.permissions.PermissionManager;
+import examples.baku.io.permissions.PermissionRequest;
import examples.baku.io.permissions.PermissionService;
import examples.baku.io.permissions.R;
import examples.baku.io.permissions.discovery.DevicePickerActivity;
@@ -61,6 +62,7 @@
public static final String KEY_MESSAGES = "messages";
private PermissionService mPermissionService;
+ private PermissionManager mPermissionManager;
private String mDeviceId;
private FirebaseDatabase mFirebaseDB;
private DatabaseReference mMessagesRef;
@@ -142,7 +144,8 @@
//Remove swiped item from list and notify the RecyclerView
int pos = viewHolder.getAdapterPosition();
MessageData item = mInboxAdapter.getItem(pos);
- if (item != null) {
+ //TOOO: bug, item id shouldn't ever be null
+ if (item != null && item.getId() != null) {
mMessagesRef.child(item.getId()).removeValue();
}
}
@@ -154,10 +157,27 @@
mPermissionService = binder.getInstance();
if (mPermissionService != null) {
mDeviceId = mPermissionService.getDeviceId();
+ mPermissionManager = mPermissionService.getPermissionManager();
mFirebaseDB = mPermissionService.getFirebaseDB();
mMessagesRef = mFirebaseDB.getReference(KEY_DOCUMENTS).child(mDeviceId).child(KEY_EMAILS).child(KEY_MESSAGES);
mMessagesRef.addValueEventListener(messagesValueListener);
mMessagesRef.addChildEventListener(messageChildListener);
+
+ mPermissionManager.addOnRequestListener("documents/" + mDeviceId + "/emails/messages/*", new PermissionManager.OnRequestListener() {
+ @Override
+ public boolean onRequest(PermissionRequest request, Blessing blessing) {
+ mInboxAdapter.notifyDataSetChanged();
+ return true;
+ }
+
+ @Override
+ public void onRequestRemoved(PermissionRequest request, Blessing blessing) {
+ mInboxAdapter.notifyDataSetChanged();
+ }
+ });
+
+ //TEMP: example status
+ mPermissionService.clearStatus(ComposeActivity.EXTRA_MESSAGE_PATH);
}
}
@@ -254,16 +274,22 @@
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 targetDevice = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ID);
String path = data.getStringExtra(DevicePickerActivity.EXTRA_REQUEST_ARGS);
- mPermissionService.getPermissionManager().bless(focus)
- .setPermissions(path, 3);
+
+ if (!mDeviceId.equals(targetDevice)) {
+ mPermissionManager.bless(targetDevice)
+ .setPermissions(path, PermissionManager.FLAG_READ)
+ .setPermissions(path + "/message", PermissionManager.FLAG_WRITE)
+ .setPermissions(path + "/subject", PermissionManager.FLAG_WRITE);
+ }
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());
+ mPermissionService.addToConstellation(targetDevice);
+ mPermissionService.getMessenger().to(targetDevice).emit("cast", castArgs.toString());
} catch (JSONException e) {
e.printStackTrace();
}
@@ -309,7 +335,7 @@
subtitleView.setText(item.getSubject());
}
- ImageView castButton = (ImageView) holder.mCardView.findViewById(R.id.card_trailing);
+ IconTextView castButton = (IconTextView) holder.mCardView.findViewById(R.id.card_trailing);
castButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -335,6 +361,16 @@
}
});
+ holder.mCardView.setCardBackgroundColor(Color.WHITE);
+ if (mPermissionManager != null) {
+ String path = "documents/" + mDeviceId + "/emails/messages/" + item.getId() + "/*";
+ for (PermissionRequest request : mPermissionManager.getRequests(path)) {
+ holder.mCardView.setCardBackgroundColor(Color.GRAY);
+ break;
+ }
+
+ }
+
}
@Override
@@ -343,6 +379,16 @@
}
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mPermissionService != null) {
+ //TEMP: example status
+ mPermissionService.clearStatus(ComposeActivity.EXTRA_MESSAGE_PATH);
+ }
+ }
+
@Override
protected void onDestroy() {
super.onDestroy();
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
index fe2a764..e367c89 100644
--- 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
@@ -17,8 +17,6 @@
*/
public class InboxFragment extends EventFragment {
- public InboxFragment(){}
-
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
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
index 62eb995..7427bf0 100644
--- 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
@@ -7,6 +7,9 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.google.firebase.database.ServerValue;
+
+import java.util.Map;
import java.util.UUID;
/**
@@ -20,6 +23,7 @@
private String source;
private String message;
private boolean callback;
+ private long timeStamp;
public Message(){}
@@ -91,17 +95,13 @@
}
-// 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;
-// }
+ public Map<String,String> getTimeStamp() {
+ return ServerValue.TIMESTAMP;
+ }
+
+ public void setTimeStamp(long timeStamp) {
+ this.timeStamp = timeStamp;
+ }
@Override
public int describeContents() {
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/util/Utils.java b/permissions/app/src/main/java/examples/baku/io/permissions/util/Utils.java
new file mode 100644
index 0000000..6fbcc31
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/util/Utils.java
@@ -0,0 +1,59 @@
+// 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+
+import com.joanzapata.iconify.IconDrawable;
+
+import java.util.Set;
+
+/**
+ * Created by phamilton on 7/20/16.
+ */
+public class Utils {
+
+ private static final int defaultIconSize = 50; //this number was chosen at random
+
+ public static Icon iconFromDrawable(Drawable drawable) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+ if (width <= 0 || height <= 0) {
+ width = defaultIconSize;
+ height = defaultIconSize;
+ }
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return Icon.createWithBitmap(bitmap);
+ }
+
+
+ //path keys are separated by '/' delimiter: a/b/c/...
+ public static String getNearestCommonAncestor(String path, Set<String> ancestors) {
+ if (path == null || ancestors.contains(path)) {
+ return path;
+ }
+ if (path.startsWith("/")) {
+ throw new IllegalArgumentException("Path can't start with /");
+ }
+ String subpath = path;
+ int index;
+ while ((index = subpath.lastIndexOf("/")) != -1) {
+ subpath = subpath.substring(0, index);
+ if (ancestors.contains(subpath)) {
+ return subpath;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/permissions/app/src/main/res/layout/activity_discovery.xml b/permissions/app/src/main/res/layout/activity_discovery.xml
new file mode 100644
index 0000000..103e0eb
--- /dev/null
+++ b/permissions/app/src/main/res/layout/activity_discovery.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/main_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".discovery.DiscoveryActivity">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/appbar_padding_top"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:layout_scrollFlags="scroll|enterAlways"
+ app:popupTheme="@style/AppTheme.PopupOverlay">
+
+ </android.support.v7.widget.Toolbar>
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </android.support.design.widget.AppBarLayout>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_margin="@dimen/fab_margin"
+ android:src="@android:drawable/ic_dialog_email" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/permissions/app/src/main/res/layout/device_card_item.xml b/permissions/app/src/main/res/layout/device_card_item.xml
index 624d931..fa67687 100644
--- a/permissions/app/src/main/res/layout/device_card_item.xml
+++ b/permissions/app/src/main/res/layout/device_card_item.xml
@@ -5,11 +5,12 @@
android:layout_height="80dp"
app:contentPadding="6dp"
app:cardUseCompatPadding="true">
- <ImageView
+ <com.joanzapata.iconify.widget.IconTextView
android:id="@+id/card_leading"
android:layout_width="62dp"
android:layout_height="62dp"
- android:src="@drawable/ic_phone_android_black_24dp"/>
+ android:textSize="62dp"
+ android:text="{md-phone-android}"/>
<TextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
diff --git a/permissions/app/src/main/res/layout/fragment_discovery.xml b/permissions/app/src/main/res/layout/fragment_discovery.xml
new file mode 100644
index 0000000..7a80c82
--- /dev/null
+++ b/permissions/app/src/main/res/layout/fragment_discovery.xml
@@ -0,0 +1,16 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context=".discovery.DiscoveryActivity$PlaceholderFragment">
+
+ <TextView
+ android:id="@+id/section_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</RelativeLayout>
diff --git a/permissions/app/src/main/res/layout/inbox_card_item.xml b/permissions/app/src/main/res/layout/inbox_card_item.xml
index fefe348..3eab804 100644
--- a/permissions/app/src/main/res/layout/inbox_card_item.xml
+++ b/permissions/app/src/main/res/layout/inbox_card_item.xml
@@ -3,38 +3,45 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="80dp"
- app:contentPadding="6dp"
- app:cardUseCompatPadding="true">
- <ImageView
+ app:cardUseCompatPadding="true"
+ app:contentPadding="6dp">
+
+ <com.joanzapata.iconify.widget.IconTextView
android:id="@+id/card_leading"
android:layout_width="62dp"
android:layout_height="62dp"
- android:src="@drawable/ic_mail_outline_black_24dp"/>
+ android:layout_gravity="center_vertical"
+ android:text="{md-mail}"
+ android:textSize="62dp" />
+
<TextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="74dp"
- android:textSize="24sp"
- android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"
- android:text=""/>
+ android:paddingLeft="74dp"
+ android:text=""
+ android:textSize="24sp"
+ android:textStyle="bold" />
+
<TextView
android:id="@+id/card_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingTop="34sp"
- android:paddingLeft="74dp"
- android:textSize="16sp"
- android:textStyle="italic"
android:ellipsize="end"
android:maxLines="1"
- android:text=""/>
- <ImageView
+ android:paddingLeft="74dp"
+ android:paddingTop="34sp"
+ android:text=""
+ android:textSize="16sp"
+ android:textStyle="italic" />
+
+ <com.joanzapata.iconify.widget.IconTextView
android:id="@+id/card_trailing"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical|right"
- android:src="@drawable/ic_cast_black_24dp"/>
+ android:text="{md-cast}"
+ android:textSize="32dp" />
</android.support.v7.widget.CardView>
\ No newline at end of file
diff --git a/permissions/app/src/main/res/menu/menu_compose.xml b/permissions/app/src/main/res/menu/menu_compose.xml
index 5072a89..1b1c832 100644
--- a/permissions/app/src/main/res/menu/menu_compose.xml
+++ b/permissions/app/src/main/res/menu/menu_compose.xml
@@ -5,7 +5,7 @@
android:id="@+id/action_cast"
android:title="Cast Message"
android:orderInCategory="100"
- android:icon="@drawable/ic_cast_white_24dp"
+ android:icon="@android:drawable/ic_menu_send"
app:showAsAction="always" />
<item
android:id="@+id/action_delete"
diff --git a/permissions/app/src/main/res/menu/menu_discovery.xml b/permissions/app/src/main/res/menu/menu_discovery.xml
new file mode 100644
index 0000000..8e30d0d
--- /dev/null
+++ b/permissions/app/src/main/res/menu/menu_discovery.xml
@@ -0,0 +1,10 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context=".discovery.DiscoveryActivity">
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:title="@string/action_settings"
+ app:showAsAction="never" />
+</menu>