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>