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