Added suggestions to the synchronized edittext fields.
With the suggestion permission, added/deleted text shows up with a highlight.
If the instance has a write permission, suggestions can be touched to trigger a dialog for accepting or rejecting them.
Fixed bugs in SyncText that were causing incorrect ordering and patches to be dropped.
Wrapped EditTextLayout in a ViewGroup class that reacts to changes in permission.

Change-Id: If8b6c4a6ffd7efa6c528209460ff8009f6659a5b
diff --git a/permissions/app/src/main/AndroidManifest.xml b/permissions/app/src/main/AndroidManifest.xml
index 4e5b397..85126f9 100644
--- a/permissions/app/src/main/AndroidManifest.xml
+++ b/permissions/app/src/main/AndroidManifest.xml
@@ -30,6 +30,7 @@
         <activity
             android:name=".examples.ComposeActivity"
             android:label="@string/title_activity_compose"
+            android:windowSoftInputMode="adjustResize"
             android:theme="@style/AppTheme.NoActionBar" />
         <activity
             android:name=".discovery.DevicePickerActivity"
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 98d08f4..6cda9a6 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
@@ -66,34 +66,29 @@
         setId(id);
         setSource(source);
         setTarget(target);
-
     }
 
-    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) {
+    public static Blessing create(PermissionManager permissionManager, String id, String source, String target) {
         Blessing blessing = permissionManager.getBlessing(source, target);
-        if (blessing == null && create) {
+        if (blessing == null) {
             blessing = new Blessing(permissionManager, id, source, target);
             permissionManager.putBlessing(blessing);
         }
         return blessing;
     }
 
+    //root blessings have no source blessing and their id is the same as their target
+    public static Blessing createRoot(PermissionManager permissionManager, String target) {
+        return create(permissionManager,target, null, target);
+    }
+
     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);
+        return create(permissionManager,id, source, target);
     }
 
     public OnBlessingUpdatedListener addListener(OnBlessingUpdatedListener listener) {
@@ -151,9 +146,6 @@
 
     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);
@@ -275,7 +267,7 @@
             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);
+            result = Blessing.create(permissionManager, null, getId(), target);
         }
         return result;
     }
@@ -288,7 +280,6 @@
         return this.target.equals(target) || parentBlessing != null && parentBlessing.isDescendantOf(target);
     }
 
-
     @Override
     public Iterator<Permission> iterator() {
         return isSynched() ? permissionTree.iterator() : null;
@@ -298,7 +289,6 @@
         return permissionTree;
     }
 
-
     public static class Permission implements Iterable<Permission> {
         String key;
         String path;
@@ -393,6 +383,7 @@
             return null;
         }
 
+
         public int getPermissions() {
             return permissions | inherited;
         }
@@ -479,7 +470,7 @@
         }
 
         public int getPermissions(String path) {
-            path = Utils.getNearestCommonAncestor(path, keySet());
+            path = Utils.getNearestCommonAncestor(path,keySet());
             Permission permission = get(path);
             if (permission == null) {
                 return 0;
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 52b1486..86767fd 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
@@ -13,6 +13,7 @@
 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;
@@ -37,6 +38,8 @@
     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_SUGGEST = 1 << 2;
+    public static final int FLAG_ROOT = Integer.MAX_VALUE;
 
     static final String KEY_PERMISSIONS = "_permissions";
     static final String KEY_REQUESTS = "_requests";
@@ -57,7 +60,7 @@
     private final Table<String, String, PermissionRequest.Builder> mActiveRequests = HashBasedTable.create();
 
     private final Multimap<String, OnRequestListener> mRequestListeners = HashMultimap.create(); //<path,, >
-    private final Multimap<String, OnRequestListener> mSubscribedRequests = HashMultimap.create(); //<request id, >
+    private final Multimap<String, OnRequestListener> mSubscribedRequests = HashMultimap.create(); //<requestDialog id, >
 
     private Blessing.PermissionTree mPermissionTree = new Blessing.PermissionTree();
     private final Multimap<String, OnPermissionChangeListener> mPermissionValueEventListeners = HashMultimap.create();
@@ -122,7 +125,7 @@
             int previous = mPermissionTree.getPermissions(path);
             int current = updatedPermissionTree.getPermissions(newPath);
             if (previous != current) {
-                changedPermissions.add(path);
+                changedPermissions.add(newPath);
             }
         }
 
@@ -141,6 +144,8 @@
         for (String path : changedPermissions) {
             onPermissionsChange(path);
         }
+
+
     }
 
     //call all the listeners effected by a permission change at this path
@@ -157,7 +162,6 @@
         }
     }
 
-
     public Set<PermissionRequest> getRequests(String path) {
         Set<PermissionRequest> result = new HashSet<>();
         for (PermissionRequest request : mRequests.values()) {
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 83a544a..575fe19 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
@@ -35,13 +35,14 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.UUID;
 
 import examples.baku.io.permissions.discovery.DeviceData;
 import examples.baku.io.permissions.discovery.DevicePickerActivity;
 import examples.baku.io.permissions.examples.ComposeActivity;
 import examples.baku.io.permissions.examples.EmailActivity;
-import examples.baku.io.permissions.messenger.Messenger;
 import examples.baku.io.permissions.messenger.Message;
+import examples.baku.io.permissions.messenger.Messenger;
 import examples.baku.io.permissions.util.Utils;
 
 public class PermissionService extends Service {
@@ -59,7 +60,6 @@
     }
 
     static final int FOREGROUND_NOTIFICATION_ID = -3278;
-    static final int FOCUS_NOTIFICATION = -43254;
 
     static final String KEY_BLESSINGS = PermissionManager.KEY_BLESSINGS;
 
@@ -72,11 +72,13 @@
 
     FirebaseDatabase mFirebaseDB;
     DatabaseReference mDevicesReference;
+    DatabaseReference mRequestsReference;
 
 
     DatabaseReference mMessengerReference;
     Messenger mMessenger;
 
+    DatabaseReference mPermissionsReference;
     PermissionManager mPermissionManager;
     Blessing mDeviceBlessing;
 
@@ -87,15 +89,16 @@
     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<>();
+    final private Map<String, DeviceData> mDiscovered = new HashMap<>();
+    final private HashSet<String> mConstellation = new HashSet<>();
+    final private Map<String, Integer> mConstellationNotifications = new HashMap<>();
 
-    private Map<String, Integer> mRequestNotifications = new HashMap<>();
+    final private HashSet<DiscoveryListener> mDiscoveryListener = new HashSet<>();
+    final private Map<String, Integer> mDiscoveredNotifications = new HashMap<>();
 
+    final private Map<String, Integer> mRequestNotifications = new HashMap<>();
 
-    private Map<String, ActionCallback> mActionListeners = new HashMap<>();
+    final private Map<String, ActionCallback> mActionListeners = new HashMap<>();
 
 
     private Icon shareIcon;
@@ -108,7 +111,7 @@
 
 
     public interface ActionCallback {
-        void onAction(Intent intent);
+        boolean onAction(Intent intent);    //return true if action is complete and the notification can be dismissed
     }
 
     public interface DiscoveryListener {
@@ -137,6 +140,7 @@
 
         mFirebaseDB = FirebaseDatabase.getInstance();
         mDevicesReference = mFirebaseDB.getReference("_devices");
+        mRequestsReference = mFirebaseDB.getReference("requests");
 
 
         shareIcon = Utils.iconFromDrawable(new IconDrawable(PermissionService.this, MaterialIcons.md_share));
@@ -149,7 +153,7 @@
 
         mPermissionManager = new PermissionManager(mFirebaseDB.getReference(), mDeviceId);
 
-        mPermissionManager.getRootBlessing().setPermissions("documents/" + mDeviceId, PermissionManager.FLAG_READ | PermissionManager.FLAG_WRITE);
+        mPermissionManager.getRootBlessing().setPermissions("documents/" + mDeviceId, PermissionManager.FLAG_ROOT);
 
         mPermissionManager.join("public");
 
@@ -181,7 +185,7 @@
 
                 Notification notification = new Notification.Builder(PermissionService.this)
                         .setSmallIcon(keyIcon)
-                        .setContentTitle("Permission request from " + sourceName)
+                        .setContentTitle("Permission requestDialog from " + sourceName)
                         .addAction(new Notification.Action.Builder(grantIcon, "Grant", acceptRequestPendingIntent).build())
 //                        .addAction(new Notification.Action.Builder(R.drawable.ic_close_black_24dp, "Reject", rejectRequestPendingIntent).build())
                         .setDeleteIntent(rejectRequestPendingIntent)
@@ -190,7 +194,6 @@
                         .build();
                 mNotificationManager.notify(nId, notification);
                 mRequestNotifications.put(request.getId(), nId);
-
                 return true;
             }
 
@@ -212,22 +215,42 @@
         mRunning = true;
     }
 
+    public void requestDialog(String requestId, String title, String subtitle, ActionCallback accept, ActionCallback reject) {
+        Integer previousNotificationId = mRequestNotifications.get(requestId);
+        if (previousNotificationId != null) {
+            mNotificationManager.cancel(previousNotificationId);
+        }
+        Notification notification = new Notification.Builder(PermissionService.this)
+                .setSmallIcon(keyIcon)
+                .setContentTitle(title)
+                .setSubText(subtitle)
+                .addAction(createAction(grantIcon, "Accept", UUID.randomUUID().toString(), accept))
+                .setDeleteIntent(createNotificationCallback(UUID.randomUUID().toString(), reject))
+                .setVibrate(new long[]{100})
+                .setPriority(Notification.PRIORITY_MAX)
+                .build();
+
+        int nId = mNotificationCounter++;
+        mNotificationManager.notify(nId, notification);
+        mRequestNotifications.put(requestId, nId);
+    }
+
+
     public Messenger getMessenger() {
         return mMessenger;
     }
 
-    public String getFocus() {
-        return mFocus;
-    }
 
-
-    public void addToConstellation(String dId) {
-        mFocus = dId;
+    public void updateConstellationDevice(String dId) {
         if (!mDiscovered.containsKey(dId)) return;
 
         DeviceData device = mDiscovered.get(dId);
         String title = device.getName();
         String subtitle = device.getId();   //default
+        if (device.getStatus() != null && device.getStatus().containsKey("description")) {
+            subtitle = device.getStatus().get("description");
+        }
+        int icon = R.drawable.ic_phone_android_black_24dp;
 
         Intent dismissIntent = new Intent(this, PermissionService.class);
         dismissIntent.putExtra("type", "dismiss");
@@ -246,9 +269,9 @@
         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() {
+            notificationBuilder.addAction(createAction(castIcon, "Cast Message", "castMessage", new ActionCallback() {
                 @Override
-                public void onAction(Intent intent) {
+                public boolean onAction(Intent intent) {
                     try {
                         JSONObject castArgs = new JSONObject();
                         castArgs.put("activity", ComposeActivity.class.getSimpleName());
@@ -257,6 +280,7 @@
                     } catch (JSONException e) {
                         e.printStackTrace();
                     }
+                    return false;
                 }
             }));
         }
@@ -269,20 +293,30 @@
             notificationBuilder.addAction(new Notification.Action.Builder(castIcon, "Pull Message", PendingIntent.getActivity(this, 0, emailIntent, PendingIntent.FLAG_CANCEL_CURRENT)).build());
         }
 
+        mConstellation.add(dId);
+        Integer notificationId = mConstellationNotifications.get(dId);
+        if (notificationId == null) {
+            notificationId = mNotificationCounter++;
+            mConstellationNotifications.put(dId, notificationId);
+        }
         Notification notification = notificationBuilder.build();
-        mNotificationManager.notify(FOCUS_NOTIFICATION, notification);
+        mNotificationManager.notify(notificationId, notification);
     }
 
-    private Notification.Action createActionCallback(Icon icon, String title, String actionId, ActionCallback callback) {
+
+    private Notification.Action createAction(Icon icon, String title, String actionId, ActionCallback callback) {
+        PendingIntent actionPendingIntent = createNotificationCallback(actionId, callback);
+        return new Notification.Action.Builder(icon, title, actionPendingIntent).build();
+    }
+
+    private PendingIntent createNotificationCallback(String actionId, ActionCallback callback) {
         mActionListeners.put(actionId, callback);
         Intent actionIntent = new Intent(this, PermissionService.class);
         actionIntent.putExtra(EXTRA_COMMAND, "actionCallback");
         actionIntent.putExtra(EXTRA_ACTION_ID, actionId);
-        PendingIntent actionPendingIntent = PendingIntent.getService(this, mActionCounter++, actionIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-        return new Notification.Action.Builder(icon, title, actionPendingIntent).build();
+        return PendingIntent.getService(this, mActionCounter++, actionIntent, PendingIntent.FLAG_CANCEL_CURRENT);
     }
 
-
     public PermissionManager getPermissionManager() {
         return mPermissionManager;
     }
@@ -349,14 +383,15 @@
 
         mMessenger.on("disassociate", new Messenger.Listener() {
             @Override
-            public void call(String args, Messenger.Ack callback) {
+            public void call(Message msg, Messenger.Ack callback) {
 
             }
         });
 
         mMessenger.on("cast", new Messenger.Listener() {
             @Override
-            public void call(String args, Messenger.Ack callback) {
+            public void call(Message msg, Messenger.Ack callback) {
+                String args = msg.getMessage();
                 if (args != null) {
                     try {
                         JSONObject jsonArgs = new JSONObject(args);
@@ -369,6 +404,9 @@
                                 startActivity(emailIntent);
                             }
                         }
+
+                        //add cast source to constellation
+                        updateConstellationDevice(msg.getSource());
                     } catch (JSONException e) {
                         e.printStackTrace();
                     }
@@ -377,6 +415,17 @@
         });
     }
 
+    public void removeFromConstellation(String deviceId) {
+        mConstellation.remove(deviceId);
+        mConstellationNotifications.remove(deviceId);
+        //revoke all blessings
+        for (Blessing blessing : mPermissionManager.getReceivedBlessings()) {
+            Blessing granted = blessing.getBlessing(deviceId);
+            if (granted != null) {
+                granted.revoke();
+            }
+        }
+    }
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
@@ -390,7 +439,13 @@
                 if (intent.hasExtra(EXTRA_ACTION_ID)) {
                     String aId = intent.getStringExtra(EXTRA_ACTION_ID);
                     if (mActionListeners.containsKey(aId)) {
-                        mActionListeners.get(aId).onAction(intent);
+                        boolean result = mActionListeners.get(aId).onAction(intent);
+                        if (result) {
+                            Integer nId = mRequestNotifications.get(aId);
+                            if (nId != null) {
+                                mNotificationManager.cancel(nId);
+                            }
+                        }
                     }
                 }
 
@@ -408,7 +463,7 @@
                     if (request != null) {
                         mPermissionManager.grantRequest(request);
                     } else {
-                        Toast.makeText(getApplicationContext(), "Expired request", 0).show();
+                        Toast.makeText(getApplicationContext(), "Expired requestDialog", 0).show();
                     }
                 }
                 if (intent.hasExtra(EXTRA_NOTIFICATION_ID)) {
@@ -423,57 +478,13 @@
             } 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();
-                        }
-                    }
+                    removeFromConstellation(dId);
+
+
                 }
 
             } else if ("close".equals(type)) {
                 stopSelf();
-            } else if ("focus".equals(type)) {
-                if (intent.hasExtra("deviceId")) {
-                    String dId = intent.getStringExtra("deviceId");
-                    l("targetting " + dId);
-                    if (mDiscovered.containsKey(dId)) {
-                        mFocus = dId;
-                        DeviceData target = mDiscovered.get(dId);
-
-                        String title = "Targetting device: " + target.getName();
-
-                        Intent contentIntent = new Intent(this, EmailActivity.class);
-
-                        Intent discoverIntent = new Intent(this, PermissionService.class);
-                        discoverIntent.putExtra("type", "discover");
-                        PendingIntent discoverPendingIntent = PendingIntent.getService(this, 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, mActionCounter++, castIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-
-                        Notification notification = new Notification.Builder(this)
-                                .setPriority(Notification.PRIORITY_HIGH)
-                                .setVibrate(new long[]{100})
-                                .setContentIntent(PendingIntent.getActivity(this, mActionCounter++, contentIntent, 0))
-                                .setSmallIcon(keyIcon)
-                                .setContentTitle(title)
-                                .addAction(new Notification.Action.Builder(castIcon, "Cast", castPendingIntent).build())
-                                .addAction(new Notification.Action.Builder(zoomIcon, "Discover", discoverPendingIntent).build())
-                                .build();
-
-                        refreshForegroundNotification(notification);
-
-                    }
-                    for (Iterator<Integer> iterator = mDiscoveredNotifications.values().iterator(); iterator.hasNext(); ) {
-                        int notId = iterator.next();
-                        mNotificationManager.cancel(notId);
-                    }
-                    mDiscoveredNotifications.clear();
-                }
             }
         }
         return super.onStartCommand(intent, flags, startId);
@@ -559,6 +570,9 @@
                         for (DiscoveryListener listener : mDiscoveryListener) {
                             listener.onChange(mDiscovered);
                         }
+                        if (mConstellation.contains(key)) {
+                            updateConstellationDevice(key);
+                        }
                     }
                 } catch (DatabaseException e) {
                     e.printStackTrace();
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/PermissionedTextLayout.java b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionedTextLayout.java
new file mode 100644
index 0000000..477bf0b
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/PermissionedTextLayout.java
@@ -0,0 +1,367 @@
+// 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.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.support.design.widget.TextInputLayout;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextWatcher;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.StrikethroughSpan;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.google.firebase.database.DatabaseError;
+import com.joanzapata.iconify.IconDrawable;
+import com.joanzapata.iconify.fonts.MaterialIcons;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+import examples.baku.io.permissions.synchronization.SyncText;
+import examples.baku.io.permissions.synchronization.SyncTextDiff;
+
+/**
+ * Created by phamilton on 7/26/16.
+ */
+public class PermissionedTextLayout extends FrameLayout implements PermissionManager.OnPermissionChangeListener, PermissionManager.OnRequestListener {
+
+    private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";
+    private int permissions = -1;
+    final Set<PermissionRequest> requests = new HashSet<>();
+    private String label;
+
+    private SyncText syncText;
+    private TextInputLayout textInputLayout;
+    private PermissionedEditText editText;
+    private FrameLayout overlay;
+
+    private ImageView actionButton;
+
+    private PermissionedTextListener permissionedTextListener = null;
+
+    private int inputType = InputType.TYPE_CLASS_TEXT;
+
+    public void unlink() {
+        if (syncText != null) {
+            syncText.unlink();
+        }
+    }
+
+
+    public interface PermissionedTextListener {
+        void onSelected(SyncTextDiff diff, PermissionedTextLayout text);
+
+        void onAction(int action, PermissionedTextLayout text);
+    }
+
+    public PermissionedTextLayout(Context context) {
+        this(context, null, 0);
+    }
+
+    public PermissionedTextLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PermissionedTextLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    public void init(final Context context, AttributeSet attrs, int defStyleAttr) {
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
+        textInputLayout = new TextInputLayout(context);
+        textInputLayout.setLayoutParams(params);
+        String hint = attrs.getAttributeValue(ANDROID_NS, "hint");
+        if (hint != null) {
+            textInputLayout.setHint(hint);
+        }
+        editText = new PermissionedEditText(context);
+        editText.setSelectionListener(selectionListener);
+        editText.setId(View.generateViewId());
+        editText.setLayoutParams(params);
+        int inputType = attrs.getAttributeIntValue(ANDROID_NS, "inputType", EditorInfo.TYPE_NULL);
+        if (inputType != EditorInfo.TYPE_NULL) {
+            editText.setInputType(inputType);
+        }
+        textInputLayout.addView(editText, params);
+        addView(textInputLayout);
+
+        overlay = new FrameLayout(context);
+        overlay.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
+        overlay.setBackgroundColor(Color.BLACK);
+        overlay.setOnTouchListener(new OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                return true;
+            }
+        });
+        overlay.setVisibility(GONE);
+        addView(overlay);
+
+        actionButton = new ImageView(context);
+        IconDrawable drawable = new IconDrawable(context, MaterialIcons.md_check);
+        drawable.color(Color.GREEN);
+        actionButton.setImageDrawable(drawable);
+        actionButton.setLayoutParams(new FrameLayout.LayoutParams(100, 100, Gravity.RIGHT));
+        actionButton.setOnTouchListener(new OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                switch (event.getAction()) {
+                    case MotionEvent.ACTION_DOWN: {
+                        ImageView view = (ImageView) v;
+                        view.getDrawable().setColorFilter(0x77000000, PorterDuff.Mode.SRC_ATOP);
+                        view.invalidate();
+                        break;
+                    }
+                    case MotionEvent.ACTION_UP:
+                        if (permissionedTextListener != null) {
+                            permissionedTextListener.onAction(0, PermissionedTextLayout.this);
+                        }
+                    case MotionEvent.ACTION_CANCEL: {
+                        ImageView view = (ImageView) v;
+                        view.getDrawable().clearColorFilter();
+                        view.invalidate();
+                        break;
+                    }
+                }
+                return true;
+            }
+        });
+        actionButton.setVisibility(GONE);
+        addView(actionButton);
+    }
+
+    public void setInputType(int inputType) {
+        this.inputType = inputType;
+    }
+
+    Spannable diffSpannable(LinkedList<SyncTextDiff> diffs) {
+        SpannableStringBuilder result = new SpannableStringBuilder();
+
+        int start;
+        String text;
+        int color = Color.YELLOW;
+        for (SyncTextDiff diff : diffs) {
+            start = result.length();
+            text = diff.text;
+            switch (diff.operation) {
+                case SyncTextDiff.DELETE:
+                    result.append(text, new BackgroundColorSpan(color), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    result.setSpan(new StrikethroughSpan(), start, start + text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    break;
+                case SyncTextDiff.INSERT:
+                    result.append(text, new BackgroundColorSpan(color), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    break;
+                case SyncTextDiff.EQUAL:
+                    result.append(text);
+                    break;
+            }
+        }
+        return result;
+    }
+
+    public void setPermissionedTextListener(PermissionedTextListener permissionedTextListener) {
+        this.permissionedTextListener = permissionedTextListener;
+    }
+
+    public void setSyncText(SyncText sync) {
+        this.syncText = sync;
+
+        syncText.setOnTextChangeListener(new SyncText.OnTextChangeListener() {
+            @Override
+            public void onTextChange(final String currentText, final LinkedList<SyncTextDiff> diffs, int ver) {
+                if (ver >= version) {
+                    updateText(diffs);
+                }
+            }
+        });
+
+        editText.addTextChangedListener(watcher);
+    }
+
+    @Override
+    public void onPermissionChange(int current) {
+        if (current != permissions) {
+            this.permissions = current;
+            update();
+        }
+    }
+
+    private void update() {
+        if (syncText != null) {
+            this.syncText.setPermissions(permissions);
+            if ((permissions & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
+                overlay.setVisibility(GONE);
+                editText.setInputType(inputType);
+                editText.setEnabled(true);
+                syncText.acceptSuggestions();
+            } else if ((permissions & PermissionManager.FLAG_SUGGEST) == PermissionManager.FLAG_SUGGEST) {
+                overlay.setVisibility(GONE);
+                editText.setInputType(inputType);
+                editText.setEnabled(true);
+            } else if ((permissions & PermissionManager.FLAG_READ) == PermissionManager.FLAG_READ) {
+                overlay.setVisibility(GONE);
+                syncText.rejectSuggestions();
+                editText.setInputType(EditorInfo.TYPE_NULL);
+                editText.setEnabled(false);
+            } else {
+                overlay.setVisibility(VISIBLE);
+                syncText.rejectSuggestions();
+                editText.setInputType(EditorInfo.TYPE_NULL);
+                editText.setEnabled(false);
+            }
+        }
+    }
+
+    private synchronized void updateText(final LinkedList<SyncTextDiff> diffs) {
+        Activity activity = getActivity();
+        if (activity != null) {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    editText.removeTextChangedListener(watcher);
+                    int prevSel = editText.getSelectionStart();
+                    editText.setText(diffSpannable(diffs));
+                    int sel = Math.min(prevSel, editText.length());
+                    if (sel > -1) {
+                        editText.setSelection(sel);
+                    }
+                    editText.addTextChangedListener(watcher);
+                }
+            });
+        }
+    }
+
+    private int version = -1;
+
+    private TextWatcher watcher = new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+        }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            version = Math.max(version, syncText.update(s.toString()));
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {
+
+        }
+    };
+
+    public void acceptSuggestions(String src) {
+        syncText.acceptSuggestions(src);
+    }
+
+
+    public void rejectSuggestions(String src) {
+        syncText.rejectSuggestions(src);
+    }
+
+    @Override
+    public void onCancelled(DatabaseError databaseError) {
+
+    }
+
+    @Override
+    public boolean onRequest(PermissionRequest request, Blessing blessing) {
+        return false;
+    }
+
+    @Override
+    public void onRequestRemoved(PermissionRequest request, Blessing blessing) {
+
+    }
+
+    private Activity getActivity() {
+        Context context = getContext();
+        while (context instanceof ContextWrapper) {
+            if (context instanceof Activity) {
+                return (Activity) context;
+            }
+            context = ((ContextWrapper) context).getBaseContext();
+        }
+        return null;
+    }
+
+    private OnSelectionChangedListener selectionListener = new OnSelectionChangedListener() {
+        @Override
+        public void onSelectionChanged(int selStart, int selEnd, boolean focus) {
+            if (focus && selStart >= 0) {
+                SyncTextDiff diff = getDiffAt(selStart);
+                if (diff != null && diff.operation != SyncTextDiff.EQUAL) {
+                    if (permissionedTextListener != null) {
+                        permissionedTextListener.onSelected(diff, PermissionedTextLayout.this);
+                    }
+                }
+            }
+        }
+    };
+
+
+    private SyncTextDiff getDiffAt(int index) {
+        int count = 0;
+        if (syncText != null) {
+            for (SyncTextDiff diff : syncText.getDiffs()) {
+                count += diff.length();
+                if (index < count) {
+                    return diff;
+                }
+            }
+        }
+        return null;
+    }
+
+    private interface OnSelectionChangedListener {
+        void onSelectionChanged(int selStart, int selEnd, boolean focus);
+    }
+
+    private class PermissionedEditText extends EditText {
+        private OnSelectionChangedListener mSelectionListener;
+
+        public PermissionedEditText(Context context) {
+            super(context);
+        }
+
+        public PermissionedEditText(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public PermissionedEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+        }
+
+        public void setSelectionListener(OnSelectionChangedListener mSelectionListener) {
+            this.mSelectionListener = mSelectionListener;
+        }
+
+        @Override
+        protected void onSelectionChanged(int selStart, int selEnd) {
+            super.onSelectionChanged(selStart, selEnd);
+            if (mSelectionListener != null) {
+                mSelectionListener.onSelectionChanged(selStart, selEnd, hasFocus());
+            }
+        }
+    }
+}
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 7d6e159..5ced936 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,6 +16,7 @@
 
 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 {
@@ -71,7 +72,7 @@
                         }
                         setResult(0, result);
                     }else{
-                        mPermissionService.addToConstellation(dId);
+                        mPermissionService.updateConstellationDevice(dId);
                     }
                     finish();
                     return true;
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DiscoveryActivity.java b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DiscoveryActivity.java
new file mode 100644
index 0000000..0f581fe
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/discovery/DiscoveryActivity.java
@@ -0,0 +1,165 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package examples.baku.io.permissions.discovery;
+
+import android.support.design.widget.TabLayout;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.Snackbar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import android.widget.TextView;
+
+import examples.baku.io.permissions.R;
+
+public class DiscoveryActivity extends AppCompatActivity {
+
+    /**
+     * The {@link android.support.v4.view.PagerAdapter} that will provide
+     * fragments for each of the sections. We use a
+     * {@link FragmentPagerAdapter} derivative, which will keep every
+     * loaded fragment in memory. If this becomes too memory intensive, it
+     * may be best to switch to a
+     * {@link android.support.v4.app.FragmentStatePagerAdapter}.
+     */
+    private SectionsPagerAdapter mSectionsPagerAdapter;
+
+    /**
+     * The {@link ViewPager} that will host the section contents.
+     */
+    private ViewPager mViewPager;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_discovery);
+
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        // Create the adapter that will return a fragment for each of the three
+        // primary sections of the activity.
+        mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
+
+        // Set up the ViewPager with the sections adapter.
+        mViewPager = (ViewPager) findViewById(R.id.container);
+        mViewPager.setAdapter(mSectionsPagerAdapter);
+
+        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
+        tabLayout.setupWithViewPager(mViewPager);
+
+        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+                        .setAction("Action", null).show();
+            }
+        });
+
+    }
+
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_discovery, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * A placeholder fragment containing a simple view.
+     */
+    public static class PlaceholderFragment extends Fragment {
+        /**
+         * The fragment argument representing the section number for this
+         * fragment.
+         */
+        private static final String ARG_SECTION_NUMBER = "section_number";
+
+        public PlaceholderFragment() {
+        }
+
+        /**
+         * Returns a new instance of this fragment for the given section
+         * number.
+         */
+        public static PlaceholderFragment newInstance(int sectionNumber) {
+            PlaceholderFragment fragment = new PlaceholderFragment();
+            Bundle args = new Bundle();
+            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
+            fragment.setArguments(args);
+            return fragment;
+        }
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                 Bundle savedInstanceState) {
+            View rootView = inflater.inflate(R.layout.fragment_discovery, container, false);
+            TextView textView = (TextView) rootView.findViewById(R.id.section_label);
+            textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
+            return rootView;
+        }
+    }
+
+    /**
+     * A {@link FragmentPagerAdapter} that returns a fragment corresponding to
+     * one of the sections/tabs/pages.
+     */
+    public class SectionsPagerAdapter extends FragmentPagerAdapter {
+
+        public SectionsPagerAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            // getItem is called to instantiate the fragment for the given page.
+            // Return a PlaceholderFragment (defined as a static inner class below).
+            return PlaceholderFragment.newInstance(position + 1);
+        }
+
+        @Override
+        public int getCount() {
+            return 2;
+        }
+
+        @Override
+        public CharSequence getPageTitle(int position) {
+            switch (position) {
+                case 0:
+                    return "Nearby";
+                case 1:
+                    return "Friends";
+            }
+            return null;
+        }
+    }
+}
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 250e62b..a04fd16 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
@@ -16,17 +16,21 @@
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.text.Editable;
+import android.text.InputType;
 import android.text.TextWatcher;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.EditText;
+import android.widget.Toast;
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
+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 com.joanzapata.iconify.IconDrawable;
 import com.joanzapata.iconify.fonts.MaterialIcons;
 
@@ -34,15 +38,18 @@
 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.PermissionRequest;
 import examples.baku.io.permissions.PermissionService;
+import examples.baku.io.permissions.PermissionedTextLayout;
 import examples.baku.io.permissions.R;
 import examples.baku.io.permissions.discovery.DevicePickerActivity;
 import examples.baku.io.permissions.synchronization.SyncText;
+import examples.baku.io.permissions.synchronization.SyncTextDiff;
 
 public class ComposeActivity extends AppCompatActivity implements ServiceConnection {
 
@@ -62,18 +69,13 @@
     private Blessing mCastBlessing;
     private Blessing mPublicBlessing;
 
-    EditText mToText;
-    EditText mFrom;
-    EditText mSubject;
-    EditText mMessage;
-
-    TextInputLayout mToLayout;
-    TextInputLayout mFromLayout;
-    TextInputLayout mSubjectLayout;
-    TextInputLayout mMessageLayout;
+    PermissionedTextLayout mTo;
+    PermissionedTextLayout mFrom;
+    PermissionedTextLayout mSubject;
+    PermissionedTextLayout mMessage;
 
     Multimap<String, PermissionRequest> mRequests = HashMultimap.create();
-    HashMap<String, TextInputLayout> mEditContainers = new HashMap<>();
+    HashMap<String, PermissionedTextLayout> mPermissionedFields = new HashMap<>();
     HashMap<String, Integer> mPermissions = new HashMap<>();
     HashMap<String, SyncText> syncTexts = new HashMap<>();
 
@@ -83,6 +85,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_compose);
         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        toolbar.setTitle("Compose");
         setSupportActionBar(toolbar);
         getSupportActionBar().setDisplayHomeAsUpEnabled(true);
         getSupportActionBar().setDisplayShowHomeEnabled(true);
@@ -97,18 +100,14 @@
             }
         });
 
+        mTo = (PermissionedTextLayout) findViewById(R.id.composeTo);
 
-        mToText = (EditText) findViewById(R.id.composeTo);
-        mToLayout = (TextInputLayout) findViewById(R.id.composeToLayout);
+        mFrom = (PermissionedTextLayout) findViewById(R.id.composeFrom);
 
-        mFrom = (EditText) findViewById(R.id.composeFrom);
-        mFromLayout = (TextInputLayout) findViewById(R.id.composeFromLayout);
+        mSubject = (PermissionedTextLayout) findViewById(R.id.composeSubject);
 
-        mSubject = (EditText) findViewById(R.id.composeSubject);
-        mSubjectLayout = (TextInputLayout) findViewById(R.id.composeSubjectLayout);
-
-        mMessage = (EditText) findViewById(R.id.composeMessage);
-        mMessageLayout = (TextInputLayout) findViewById(R.id.composeMessageLayout);
+        mMessage = (PermissionedTextLayout) findViewById(R.id.composeMessage);
+        mMessage.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 
         bindService(new Intent(this, PermissionService.class), this, Service.BIND_AUTO_CREATE);
     }
@@ -154,7 +153,7 @@
 
 
     void sendMessage() {
-        //TODO: PermissionManager.request()
+        //TODO: PermissionManager.requestDialog()
         mPermissionManager.request(mPath + "/send", mDeviceId)
                 .putExtra(PermissionManager.EXTRA_TIMEOUT, "2000")
                 .putExtra(PermissionManager.EXTRA_COLOR, "#F00");
@@ -175,19 +174,17 @@
                     mCastBlessing = mPermissionManager.getRootBlessing();
                 }
                 mCastBlessing.bless(targetDevice)
-                        .setPermissions(mPath, PermissionManager.FLAG_READ)
-                        .setPermissions(mPath + "/message", PermissionManager.FLAG_WRITE)
-                        .setPermissions(mPath + "/subject", PermissionManager.FLAG_WRITE);
+                        .setPermissions(mPath + "/to", PermissionManager.FLAG_READ)
+                        .setPermissions(mPath + "/subject", PermissionManager.FLAG_SUGGEST)
+                        .setPermissions(mPath + "/message", PermissionManager.FLAG_SUGGEST);
             }
 
             JSONObject castArgs = new JSONObject();
             try {
-
                 castArgs.put("activity", ComposeActivity.class.getSimpleName());
                 castArgs.put(EXTRA_MESSAGE_PATH, mPath);
                 mPermissionService.getMessenger().to(targetDevice).emit("cast", castArgs.toString());
-
-                mPermissionService.addToConstellation(targetDevice);
+                mPermissionService.updateConstellationDevice(targetDevice);
             } catch (JSONException e) {
                 e.printStackTrace();
             }
@@ -233,15 +230,15 @@
             }
 
             mMessageRef = mPermissionService.getFirebaseDB().getReference(mPath);
-            mSyncedMessageRef = mMessageRef.child("syncedValues");
+            mSyncedMessageRef = mPermissionService.getFirebaseDB().getReference("documents/" + mOwner + "/emails/syncedMessages/" + mId);
             mPermissionManager.addPermissionEventListener(mPath, messagePermissionListener);
-            wrapTextField(mToLayout, "to");
-            wrapTextField(mFromLayout, "from");
-            wrapTextField(mSubjectLayout, "subject");
-            wrapTextField(mMessageLayout, "message");
+            initField(mTo, "to");
+            initField(mFrom, "from");
+            initField(mSubject, "subject");
+            initField(mMessage, "message");
 
-            mPublicBlessing = mPermissionManager.bless("public")
-                    .setPermissions(mPath + "/subject", PermissionManager.FLAG_READ);
+//            mPublicBlessing = mPermissionManager.bless("public")
+//                    .setPermissions(mPath + "/subject", PermissionManager.FLAG_READ);
 
             mPermissionManager.addOnRequestListener("documents/" + mDeviceId + "/emails/messages/" + mId + "/*", new PermissionManager.OnRequestListener() {
                 @Override
@@ -281,52 +278,42 @@
 
     }
 
-    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();
-
-        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();
+    void initField(final PermissionedTextLayout edit, final String key) {
+        edit.setSyncText(new SyncText(mDeviceId, PermissionManager.FLAG_SUGGEST, mSyncedMessageRef.child(key), mMessageRef.child(key)));
+        edit.setPermissionedTextListener(new PermissionedTextLayout.PermissionedTextListener() {
+            @Override
+            public void onSelected(final SyncTextDiff diff, PermissionedTextLayout text) {
+                int current = mPermissionManager.getPermissions(mPath + "/" + key);
+                if ((current & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
+                    mPermissionService.requestDialog(diff.source + "@" + key, "Apply changes from " + diff.source, "be vigilant",
+                            new PermissionService.ActionCallback() {
+                                @Override
+                                public boolean onAction(Intent intent) {
+                                    edit.acceptSuggestions(diff.source);
+                                    return true;
+                                }
+                            }, new PermissionService.ActionCallback() {
+                                @Override
+                                public boolean onAction(Intent intent) {
+                                    edit.rejectSuggestions(diff.source);
+                                    return true;
+                                }
+                            });
                 }
-            });
-            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);
+            @Override
+            public void onAction(int action, PermissionedTextLayout text) {
+            }
+        });
+
+        mPermissionedFields.put(key, edit);
         final String path = "documents/" + mDeviceId + "/emails/messages/" + mId + "/" + key;
 
         mPermissionManager.addPermissionEventListener(mPath + "/" + key, new PermissionManager.OnPermissionChangeListener() {
             @Override
             public void onPermissionChange(int current) {
-                mPermissions.put(path, current);
-                updateTextField(key);
+                edit.onPermissionChange(current);
             }
 
             @Override
@@ -336,57 +323,10 @@
         });
     }
 
-    void unlinkTextField(String key) {
-        if (syncTexts.containsKey(key)) {
-            syncTexts.get(key).unlink();
+    public void unlink() {
+        for (PermissionedTextLayout text : mPermissionedFields.values()) {
+            text.unlink();
         }
-    }
-
-    void linkTextField(final EditText edit, final String key) {
-        final SyncText syncText = new SyncText(mSyncedMessageRef.child(key), mMessageRef.child(key));
-        syncTexts.put(key, syncText);
-
-        syncText.setOnTextChangeListener(new SyncText.OnTextChangeListener() {
-            @Override
-            public void onTextChange(final String currentText) {
-                final int sel = Math.min(edit.getSelectionStart(), currentText.length());
-                runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        edit.setText(currentText);
-                        if (sel > -1) {
-                            edit.setSelection(sel);
-                        }
-                    }
-                });
-            }
-        });
-
-        edit.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
-            }
-
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-                syncText.update(s.toString());
-            }
-
-            @Override
-            public void afterTextChanged(Editable s) {
-
-            }
-        });
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        unlinkTextField("to");
-        unlinkTextField("form");
-        unlinkTextField("subject");
-        unlinkTextField("message");
         if (mPermissionService != null) {
             if (mPublicBlessing != null) {
                 mPublicBlessing.revokePermissions(mPath);
@@ -397,4 +337,10 @@
         }
         unbindService(this);
     }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unlink();
+    }
 }
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 c42623f..e152faa 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
@@ -55,7 +55,7 @@
 
     static void l(String msg) {
         Log.e(TAG, msg);
-    }   //TODO: real logging
+    }
 
     public static final String KEY_DOCUMENTS = "documents";
     public static final String KEY_EMAILS = "emails";
@@ -81,6 +81,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_permission);
         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        toolbar.setTitle("Inbox");
         setSupportActionBar(toolbar);
 
         FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
@@ -280,15 +281,15 @@
 
             if (!mDeviceId.equals(targetDevice)) {
                 mPermissionManager.bless(targetDevice)
-                        .setPermissions(path, PermissionManager.FLAG_READ)
-                        .setPermissions(path + "/message", PermissionManager.FLAG_WRITE)
-                        .setPermissions(path + "/subject", PermissionManager.FLAG_WRITE);
+                        .setPermissions(path + "/to", PermissionManager.FLAG_READ)
+                        .setPermissions(path + "/message", PermissionManager.FLAG_SUGGEST)
+                        .setPermissions(path + "/subject", PermissionManager.FLAG_SUGGEST);
             }
             JSONObject castArgs = new JSONObject();
             try {
                 castArgs.put("activity", ComposeActivity.class.getSimpleName());
                 castArgs.put(ComposeActivity.EXTRA_MESSAGE_PATH, path);
-                mPermissionService.addToConstellation(targetDevice);
+                mPermissionService.updateConstellationDevice(targetDevice);
                 mPermissionService.getMessenger().to(targetDevice).emit("cast", castArgs.toString());
             } catch (JSONException e) {
                 e.printStackTrace();
@@ -394,4 +395,4 @@
         super.onDestroy();
         unbindService(this);
     }
-}
+}
\ No newline at end of file
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 e367c89..88fd8aa 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,7 +17,6 @@
  */
 public class InboxFragment extends EventFragment {
 
-
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java
index faef35f..13c8a08 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/messenger/Messenger.java
@@ -150,7 +150,7 @@
                     }
                 };
             }
-            mListeners.get(event).call(message.getMessage(), callback);
+            mListeners.get(event).call(message, callback);
             return true;
 
         //assume that none of the event listeners match the uuid of a message
@@ -195,7 +195,7 @@
     }
 
     public interface Listener{
-        void call(String args, Ack callback);
+        void call(Message msg, Ack callback);
     }
 
     public interface Ack{
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java
index 7cc639c..7d25511 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncText.java
@@ -4,22 +4,31 @@
 
 package examples.baku.io.permissions.synchronization;
 
+import android.util.Log;
+
 import com.google.firebase.database.ChildEventListener;
 import com.google.firebase.database.DataSnapshot;
 import com.google.firebase.database.DatabaseError;
 import com.google.firebase.database.DatabaseException;
 import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.GenericTypeIndicator;
 import com.google.firebase.database.MutableData;
 import com.google.firebase.database.Transaction;
 import com.google.firebase.database.ValueEventListener;
 
 import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
 
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
 import java.util.UUID;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
+import examples.baku.io.permissions.PermissionManager;
+
 /**
  * Created by phamilton on 6/24/16.
  */
@@ -30,10 +39,13 @@
     static final String KEY_VERSION = "version";
     static final String KEY_PATCHES = "patches";
     static final String KEY_SUBSCRIBERS = "subscribers";
+    static final String KEY_DIFFS = "diffs";
 
-    private String text = "";
+    private final GenericTypeIndicator<ArrayList<SyncTextDiff>> diffListType = new GenericTypeIndicator<ArrayList<SyncTextDiff>>() {
+    };
+
+    private LinkedList<SyncTextDiff> diffs = new LinkedList<>();
     private int ver;
-    private String original = text;
     private BlockingQueue<SyncTextPatch> mPatchQueue;
 
     private DiffMatchPatch diffMatchPatch = new DiffMatchPatch();
@@ -46,20 +58,20 @@
 
     private OnTextChangeListener mOnTextChangeListener;
 
-    private String mId;
-
-    private String mInstance;
+    private String mInstanceId;
+    private String mLocalSource;
+    private int mPermissions;
 
 
-    public SyncText(DatabaseReference reference, DatabaseReference output){
-        if(reference == null) throw new IllegalArgumentException("null reference");
+    public SyncText(String local, int permissions, DatabaseReference reference, DatabaseReference output) {
+        if (reference == null) throw new IllegalArgumentException("null reference");
 
-        mInstance = UUID.randomUUID().toString();
-
+        mLocalSource = local;
+        mPermissions = permissions;
         mSyncRef = reference;
         mOutputRef = output;
 
-        mId = UUID.randomUUID().toString();
+        mInstanceId = UUID.randomUUID().toString();
 
         mPatchQueue = new LinkedBlockingQueue<>();
         mPatchConsumer = new PatchConsumer(mPatchQueue);
@@ -69,128 +81,126 @@
         link();
     }
 
-    public String getText() {
-        return text;
+
+    public LinkedList<SyncTextDiff> getDiffs() {
+        return diffs;
     }
 
-    public void setText(String text) {
-        this.text = text;
+    public int getPermissions() {
+        return mPermissions;
     }
 
-    public int getVer() {
-        return ver;
+    public void setPermissions(int mPermissions) {
+        this.mPermissions = mPermissions;
+        if ((mPermissions & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
+            acceptSuggestions(mLocalSource);
+        }
     }
 
-    public void setVer(int ver) {
-        this.ver = ver;
+    public static String getFinalText(LinkedList<SyncTextDiff> diffs) {
+        String result = "";
+        for (SyncTextDiff diff : diffs) {
+            if (diff.operation == SyncTextDiff.EQUAL) {
+                result += diff.getText();
+            }
+        }
+        return result;
     }
 
     public void setOnTextChangeListener(OnTextChangeListener onTextChangeListener) {
         this.mOnTextChangeListener = onTextChangeListener;
     }
 
-    public void update(String newText){
-        if(mPatchesRef == null){
+    public int update(String newText) {
+        if (mPatchesRef == null) {
             throw new RuntimeException("database connection hasn't been initialized");
         }
 
-        LinkedList<DiffMatchPatch.Patch> patches = diffMatchPatch.patchMake(text, newText);
+        LinkedList<DiffMatchPatch.Patch> patches = diffMatchPatch.patchMake(fromDiffs(this.diffs), newText);
 
-        if(patches.size() > 0){
+        if (patches.size() > 0) {
             String patchString = diffMatchPatch.patchToText(patches);
             SyncTextPatch patch = new SyncTextPatch();
             patch.setVer(ver + 1);
             patch.setPatch(patchString);
+            if (mLocalSource != null) {
+                patch.setSource(mLocalSource);
+            }
+            patch.setPermissions(mPermissions);
             mPatchesRef.push().setValue(patch);
+            return patch.getVer();
         }
+        return -1;
     }
 
-    private void processPatch(SyncTextPatch patch){
-
-        int v = patch.getVer();
-        if(this.ver >= v){  //ignore patches for previous versions
-            return;
-        }
-
-        LinkedList<DiffMatchPatch.Patch> remotePatch = new LinkedList<>(diffMatchPatch.patchFromText(patch.getPatch()));
-        Object[] results = diffMatchPatch.patchApply(remotePatch, this.text);
-        //TODO: check results
-        if(results != null && results.length > 0 && results[0] instanceof String){
-            String patchedString = (String)results[0];
-            this.ver = v;
-            this.text = patchedString;
-            updateCurrent();
-        }
-    }
-
-    private void updateCurrent(){
+    //TODO: this method currently waits for server confirmation to notify listeners. Ideally, it should notify immediately and revert on failure
+    private void updateCurrent(final int ver, final LinkedList<SyncTextDiff> diffs) {
+        final String text = getFinalText(diffs);
+        this.ver = ver;
+        this.diffs = diffs;
+        mSyncRef.child(KEY_CURRENT).removeEventListener(mCurrentValueListener);
         mSyncRef.child(KEY_CURRENT).runTransaction(new Transaction.Handler() {
             @Override
             public Transaction.Result doTransaction(MutableData currentData) {
-                if(currentData.getValue() == null){
+                if (currentData.getValue() == null) {
                     currentData.child(KEY_TEXT).setValue(text);
                     currentData.child(KEY_VERSION).setValue(ver);
-                }else{
+                    currentData.child(KEY_DIFFS).setValue(diffs);
+
+                } else {
                     int latest = currentData.child(KEY_VERSION).getValue(Integer.class);
-                    if(latest > ver){
+                    if (latest > ver) {
                         return Transaction.abort();
                     }
                     currentData.child(KEY_TEXT).setValue(text);
                     currentData.child(KEY_VERSION).setValue(ver);
+                    currentData.child(KEY_DIFFS).setValue(diffs);
                 }
                 return Transaction.success(currentData);
             }
 
             @Override
             public void onComplete(DatabaseError databaseError, boolean success, DataSnapshot dataSnapshot) {
-                if(success){
-                    if(mOnTextChangeListener != null){
-                        mOnTextChangeListener.onTextChange(text);
-                    }
-                    if(mOutputRef != null){  //pass successful change to output location
-                        mOutputRef.setValue(text);
-                    }
+                if (success) {
+                    notifyListeners(diffs, ver);
                 }
+                mSyncRef.child(KEY_CURRENT).addValueEventListener(mCurrentValueListener);
             }
         });
     }
 
-    public void link(){
+    private void notifyListeners(LinkedList<SyncTextDiff> diffs, int ver) {
+        String text = getFinalText(diffs);
+        if (mOnTextChangeListener != null) {
+            mOnTextChangeListener.onTextChange(text, diffs, ver);
+        }
+        if (mOutputRef != null) {  //pass successful change to output location
+            mOutputRef.setValue(text);
+        }
+    }
 
-        mSyncRef.child(KEY_SUBSCRIBERS).child(mId).setValue(0);
+    public void link() {
+
+        mSyncRef.child(KEY_SUBSCRIBERS).child(mInstanceId).setValue(0);
 
         mPatchesRef = mSyncRef.child(KEY_PATCHES);
         mSyncRef.child(KEY_CURRENT).addListenerForSingleValueEvent(new ValueEventListener() {
             @Override
             public void onDataChange(DataSnapshot dataSnapshot) {
-                if(dataSnapshot.exists()){
-                    text = dataSnapshot.child(KEY_TEXT).getValue(String.class);
+                if (dataSnapshot.exists()) {
+                    if (dataSnapshot.hasChild(KEY_DIFFS)) {
+                        diffs = new LinkedList<SyncTextDiff>(dataSnapshot.child(KEY_DIFFS).getValue(diffListType));
+                    }
                     ver = dataSnapshot.child(KEY_VERSION).getValue(Integer.class);
-                }else if(mOutputRef != null){   //check if output ref already has a value
-                    mOutputRef.addListenerForSingleValueEvent(new ValueEventListener() {
-                        @Override
-                        public void onDataChange(DataSnapshot dataSnapshot) {
-                            if(dataSnapshot.exists() && dataSnapshot.getValue() != null){
-                                text = dataSnapshot.getValue(String.class);
-                                original = text;
-                            }
-                            updateCurrent();
-                        }
-                        @Override
-                        public void onCancelled(DatabaseError databaseError) {
-
-                        }
-                    });
-                }else{  //version 0, empty string
-                    updateCurrent();
+                } else {  //version 0, empty string
+                    updateCurrent(0, new LinkedList<>(diffs));
                 }
 
+                notifyListeners(diffs, ver);
+
 //                mPatchesRef.orderByChild(KEY_VERSION).startAt(ver).addChildEventListener(mPatchListener);
                 mPatchesRef.addChildEventListener(mPatchListener);
-
-                if(mOnTextChangeListener != null){
-                    mOnTextChangeListener.onTextChange(text);
-                }
+                mSyncRef.child(KEY_CURRENT).addValueEventListener(mCurrentValueListener);
             }
 
             @Override
@@ -200,24 +210,38 @@
         });
     }
 
-    public String getOriginal() {
-        return original;
-    }
+    private ValueEventListener mCurrentValueListener = new ValueEventListener() {
+        @Override
+        public void onDataChange(DataSnapshot dataSnapshot) {
+            if (dataSnapshot.exists()) {
+                int version = dataSnapshot.child(KEY_VERSION).getValue(Integer.class);
+                if (version > ver && dataSnapshot.hasChild(KEY_DIFFS)) {
+                    ver = version;
+                    diffs = new LinkedList<SyncTextDiff>(dataSnapshot.child(KEY_DIFFS).getValue(diffListType));
+                    notifyListeners(diffs, ver);
+                }
+            }
+        }
+
+        @Override
+        public void onCancelled(DatabaseError databaseError) {
+
+        }
+    };
 
     private ChildEventListener mPatchListener = new ChildEventListener() {
         @Override
         public void onChildAdded(DataSnapshot dataSnapshot, String s) {
-            if(dataSnapshot.exists()){
-                try{
-                    SyncTextPatch patch = dataSnapshot.getValue(SyncTextPatch.class);
-                    if(patch != null){
-                        mPatchQueue.add(patch);
-                    }
-                }catch(DatabaseException e){
-                    e.printStackTrace();
+            try {
+                SyncTextPatch patch = dataSnapshot.getValue(SyncTextPatch.class);
+                if (patch != null) {
+                    mPatchQueue.add(patch);
                 }
+            } catch (DatabaseException e) {
+                e.printStackTrace();
             }
 
+            dataSnapshot.getRef().removeValue();
         }
 
         @Override
@@ -241,28 +265,206 @@
         }
     };
 
-    public void unlink(){
+    public void unlink() {
         mSyncRef.child(KEY_PATCHES).removeEventListener(mPatchListener);
+        mSyncRef.child(KEY_SUBSCRIBERS).child(mInstanceId).removeValue();
     }
 
-    public interface OnTextChangeListener{
-        void onTextChange(String currentText);
+    public interface OnTextChangeListener {
+        void onTextChange(String finalText, LinkedList<SyncTextDiff> diffs, int ver);
     }
 
     private class PatchConsumer implements Runnable {
         private final BlockingQueue<SyncTextPatch> queue;
 
-        PatchConsumer(BlockingQueue q) { queue = q; }
+        PatchConsumer(BlockingQueue q) {
+            queue = q;
+        }
+
         public void run() {
             try {
-                while (true) { consume(queue.take()); }
+                while (true) {
+                    consume(queue.take());
+                }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
+
         void consume(SyncTextPatch patch) {
             processPatch(patch);
         }
     }
 
+    private static String fromDiffs(List<SyncTextDiff> diffs) {
+        String result = "";
+        for (SyncTextDiff diff : diffs) {
+            result += diff.getText();
+        }
+        return result;
+    }
+
+    boolean hasWrite(SyncTextPatch patch) {
+        return (patch.getPermissions() & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE;
+    }
+
+    //TODO: bug when duplicate letter patterns in the text. The diff algorithm doesn't take source into account.
+    //TODO: this method doesn't handle delete operations on diffs with different sources (e.g. deleting a suggestion from another source), these operations are currently ignored
+    void processPatch(SyncTextPatch patch) {
+        int v = patch.getVer();
+        if (this.ver >= v) {  //ignore patches for previous versions
+            return;
+        }
+
+        String previous = fromDiffs(this.diffs);
+        String source = patch.getSource();
+        LinkedList<DiffMatchPatch.Patch> patches = new LinkedList<>(diffMatchPatch.patchFromText(patch.getPatch()));
+        Object[] patchResults = diffMatchPatch.patchApply(patches, previous);
+
+        if (patchResults == null) {   //return if failed to apply patch
+            return;
+        }
+
+        String patched = (String) patchResults[0];
+        LinkedList<DiffMatchPatch.Diff> diffs = diffMatchPatch.diffMain(previous, patched);
+        LinkedList<SyncTextDiff> result = new LinkedList<>();
+        ListIterator<SyncTextDiff> previousIterator = new LinkedList<>(this.diffs).listIterator();
+        SyncTextDiff previousDiff = null;
+        SyncTextDiff last = null;
+
+        int length;
+
+        for (DiffMatchPatch.Diff current : diffs) {
+            int operation = current.operation.ordinal();
+            String value = current.text;
+
+            if (previousDiff == null && previousIterator.hasNext()) {
+                previousDiff = previousIterator.next();
+            }
+
+            switch (operation) {
+                case SyncTextDiff.EQUAL:
+                    length = value.length();
+                    while (previousDiff.length() < length) {
+                        result.add(previousDiff);
+                        length -= previousDiff.length();
+                        previousDiff = previousIterator.next();
+                    }
+                    if (previousDiff.length() == length) {
+                        result.add(previousDiff);
+                        if (previousIterator.hasNext()) {
+                            previousDiff = previousIterator.next();
+                        }
+                    } else {
+                        SyncTextDiff splitDiff = previousDiff.truncate(length);
+                        result.add(previousDiff);
+                        previousDiff = splitDiff;
+                    }
+                    break;
+                case SyncTextDiff.INSERT:
+                    last = result.peekLast();
+                    if (last != null && last.compatible(operation, source) && !hasWrite(patch)) {
+                        last.text += value;
+                    } else if (hasWrite(patch)) {
+                        result.add(new SyncTextDiff(current.text, SyncTextDiff.EQUAL, source, patch.getPermissions()));
+                    } else {
+                        result.add(new SyncTextDiff(current.text, operation, source, patch.getPermissions()));
+                    }
+                    break;
+                case SyncTextDiff.DELETE:
+                    length = value.length();
+                    while (previousDiff.length() <= length) {
+                        if (!hasWrite(patch) && (!source.equals(previousDiff.source) || previousDiff.operation != SyncTextDiff.INSERT)) {
+                            previousDiff.setSource(source);
+                            previousDiff.setOperation(operation);
+                            result.add(previousDiff);
+                        }
+                        length -= previousDiff.length();
+
+                        if (previousIterator.hasNext()) {
+                            previousDiff = previousIterator.next();
+                        }
+                    }
+                    if (length > 0) {
+                        if (!hasWrite(patch) && (!source.equals(previousDiff.source) || previousDiff.operation != SyncTextDiff.INSERT)) {
+                            SyncTextDiff splitDiff = previousDiff.truncate(length);
+                            previousDiff.setSource(source);
+                            previousDiff.setOperation(operation);
+                            result.add(previousDiff);
+                            previousDiff = splitDiff;
+                        } else {
+                            previousDiff.text = previousDiff.text.substring(0, length);
+                        }
+                    }
+                    break;
+            }
+        }
+
+        //merge compatible diffs
+        reduceDiffs(result);
+
+        updateCurrent(v, result);
+
+    }
+
+    static void reduceDiffs(LinkedList<SyncTextDiff> diffs) {
+        if (!diffs.isEmpty()) {
+            Iterator<SyncTextDiff> iterator = diffs.iterator();
+            SyncTextDiff neighbor = iterator.next();
+            while (iterator.hasNext()) {
+                SyncTextDiff diff = iterator.next();
+                if (neighbor.compatible(diff)) {
+                    neighbor.text += diff.text;
+                    iterator.remove();
+                } else {
+                    neighbor = diff;
+                }
+            }
+        }
+    }
+
+    public void acceptSuggestions() {
+        acceptSuggestions(mLocalSource);
+    }
+
+    public void acceptSuggestions(String source) {
+        LinkedList<SyncTextDiff> result = new LinkedList<>(diffs);
+        for (Iterator<SyncTextDiff> iterator = result.iterator(); iterator.hasNext(); ) {
+            SyncTextDiff diff = iterator.next();
+            if (diff.source.equals(source)) {
+                switch (diff.operation) {
+                    case SyncTextDiff.DELETE:
+                        iterator.remove();
+                        break;
+                    default:
+                        diff.operation = SyncTextDiff.EQUAL;
+                        break;
+                }
+            }
+        }
+        updateCurrent(ver + 1, result);
+    }
+
+    public void rejectSuggestions() {
+        rejectSuggestions(mLocalSource);
+    }
+
+    public void rejectSuggestions(String source) {
+        LinkedList<SyncTextDiff> result = new LinkedList<>(diffs);
+        for (Iterator<SyncTextDiff> iterator = result.iterator(); iterator.hasNext(); ) {
+            SyncTextDiff diff = iterator.next();
+            if (diff.source.equals(source)) {
+                switch (diff.operation) {
+                    case SyncTextDiff.DELETE:
+                        diff.operation = SyncTextDiff.EQUAL;
+                        break;
+                    case SyncTextDiff.INSERT:
+                        iterator.remove();
+                        break;
+                }
+            }
+        }
+        updateCurrent(ver + 1, result);
+    }
+
 }
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextDiff.java b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextDiff.java
new file mode 100644
index 0000000..6f4487e
--- /dev/null
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextDiff.java
@@ -0,0 +1,115 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package examples.baku.io.permissions.synchronization;
+
+import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
+
+import java.util.Objects;
+
+/**
+ * Created by phamilton on 8/5/16.
+ */
+public class SyncTextDiff {
+
+    public final static int DELETE = 0;
+    public final static int INSERT = 1;
+    public final static int EQUAL = 2;
+
+    public String text;
+    public int operation;
+    public String source;
+    public int permission;
+
+    public SyncTextDiff() {
+    }
+
+    public SyncTextDiff(String text, int operation, String source, int permission) {
+        this.text = text;
+        this.operation = operation;
+        this.source = source;
+        this.permission = permission;
+    }
+
+    public SyncTextDiff(SyncTextDiff other) {
+        this.text = other.text;
+        this.operation = other.operation;
+        this.source = other.source;
+        this.permission = other.permission;
+    }
+
+
+    public int getPermission() {
+        return permission;
+    }
+
+    public void setPermission(int permission) {
+        this.permission = permission;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public int getOperation() {
+        return operation;
+    }
+
+    public void setOperation(int operation) {
+        this.operation = operation;
+    }
+
+    public int length() {
+        return text == null ? 0 : text.length();
+    }
+
+    public boolean compatible(SyncTextDiff other){
+        return compatible(other.operation, other.source);
+    }
+
+    public boolean compatible(int operation, String source) {
+        return operation == this.operation
+                && Objects.equals(this.source, source);
+    }
+
+    public SyncTextDiff truncate(int start) {
+        SyncTextDiff result = new SyncTextDiff();
+        result.setSource(source);
+        result.setOperation(operation);
+        result.setText(text.substring(start));
+        setText(text.substring(0, start));
+        return result;
+    }
+
+    public static SyncTextDiff fromDiff(DiffMatchPatch.Diff diff, String src, String target) {
+        SyncTextDiff result = new SyncTextDiff();
+        result.setText(diff.text);
+        result.setSource(src);
+        int op = EQUAL;
+        switch (diff.operation) {
+            case DELETE:
+                op = SyncTextDiff.DELETE;
+                break;
+            case INSERT:
+                op = SyncTextDiff.INSERT;
+        }
+        result.setOperation(op);
+        return result;
+    }
+
+
+}
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java
index 48ef0b6..dd0385e 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/synchronization/SyncTextPatch.java
@@ -9,12 +9,22 @@
 /**
  * Created by phamilton on 6/24/16.
  */
-public class SyncTextPatch{
+public class SyncTextPatch {
     private int ver;
     private String patch;
     private String source;
+    private int permissions;
 
-    public SyncTextPatch() {}
+    public int getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(int permissions) {
+        this.permissions = permissions;
+    }
+
+    public SyncTextPatch() {
+    }
 
     public int getVer() {
         return ver;
diff --git a/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java b/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java
index 6d50328..90c353c 100644
--- a/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java
+++ b/permissions/app/src/main/java/examples/baku/io/permissions/util/EventFragment.java
@@ -10,14 +10,13 @@
 
 /**
  * Created by phamilton on 6/26/16.
- *
  * Fragment that requires the binding context to implement an event handler.
  */
-public class EventFragment extends Fragment{
+public class EventFragment extends Fragment {
 
     EventFragmentListener mListener;
 
-    public interface EventFragmentListener{
+    public interface EventFragmentListener {
         boolean onFragmentEvent(int action, Bundle args, EventFragment fragment);
     }
 
@@ -31,9 +30,7 @@
         }
     }
 
-    public boolean onEvent(int action, Bundle args){
-        if(mListener == null)
-            return false;
-        return mListener.onFragmentEvent(action, args, this);
+    public boolean onEvent(int action, Bundle args) {
+        return mListener != null && mListener.onFragmentEvent(action, args, this);
     }
 }
diff --git a/permissions/app/src/main/res/layout/content_compose.xml b/permissions/app/src/main/res/layout/content_compose.xml
index 3e3f476..a869c90 100644
--- a/permissions/app/src/main/res/layout/content_compose.xml
+++ b/permissions/app/src/main/res/layout/content_compose.xml
@@ -13,59 +13,38 @@
     tools:showIn="@layout/activity_compose">
 
 
-    <android.support.design.widget.TextInputLayout
-        android:id="@+id/composeToLayout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <EditText
+        <examples.baku.io.permissions.PermissionedTextLayout
             android:id="@+id/composeTo"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="textEmailAddress"
             android:hint="To" />
-    </android.support.design.widget.TextInputLayout>
 
-    <android.support.design.widget.TextInputLayout
-        android:id="@+id/composeFromLayout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/composeToLayout">
-        <EditText
+        <examples.baku.io.permissions.PermissionedTextLayout
             android:id="@+id/composeFrom"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="textEmailAddress"
+            android:layout_below="@id/composeTo"
             android:hint="From" />
-    </android.support.design.widget.TextInputLayout>
 
-    <android.support.design.widget.TextInputLayout
-        android:id="@+id/composeSubjectLayout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/composeFromLayout">
 
-        <EditText
+        <examples.baku.io.permissions.PermissionedTextLayout
             android:id="@+id/composeSubject"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="textEmailSubject"
+            android:layout_below="@id/composeFrom"
             android:hint="Subject" />
-    </android.support.design.widget.TextInputLayout>
 
-    <android.support.design.widget.TextInputLayout
-        android:id="@+id/composeMessageLayout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/composeSubjectLayout">
 
-        <EditText
+        <examples.baku.io.permissions.PermissionedTextLayout
             android:id="@+id/composeMessage"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="textMultiLine"
+            android:layout_below="@id/composeSubject"
             android:hint="Message" />
-    </android.support.design.widget.TextInputLayout>
 
 
 
diff --git a/permissions/app/src/main/res/values/attrs.xml b/permissions/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..e3a1235
--- /dev/null
+++ b/permissions/app/src/main/res/values/attrs.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="PermissionedTextLayout">
+        <attr name="askldnaskdnl" format="boolean" />
+        <attr name="labelPosition" format="enum">
+            <enum name="left" value="0" />
+            <enum name="right" value="1" />
+        </attr>
+    </declare-styleable>
+</resources>
\ No newline at end of file