Enable correction/deleting notification via EasyEditSpan.

When the "delete" pop-up is clicked (and the wrapped text removed), the
creator of the span will receive a notification of the action.

Similarly, if the user modifies (i.e., add/remove a char), the creator of
the span will receive a notification too. The notification will not contain any
information about how the text has been modified.


Bug: 6905960
Change-Id: Ic227b8fd50066699915f69a54f225fb5330867c4
diff --git a/api/current.txt b/api/current.txt
index a6119b6..551d8cb 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -22969,9 +22969,13 @@
 
   public class EasyEditSpan implements android.text.ParcelableSpan {
     ctor public EasyEditSpan();
+    ctor public EasyEditSpan(android.app.PendingIntent);
+    ctor public EasyEditSpan(android.os.Parcel);
     method public int describeContents();
     method public int getSpanTypeId();
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final java.lang.String ACTION_TEXT_DELETED = "android.text.style.TEXT_DELETED";
+    field public static final java.lang.String ACTION_TEXT_MODIFIED = "android.text.style.TEXT_MODIFIED";
   }
 
   public class ForegroundColorSpan extends android.text.style.CharacterStyle implements android.text.ParcelableSpan android.text.style.UpdateAppearance {
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 1508d10..2ab9bf8 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -757,7 +757,7 @@
                     break;
 
                 case EASY_EDIT_SPAN:
-                    readSpan(p, sp, new EasyEditSpan());
+                    readSpan(p, sp, new EasyEditSpan(p));
                     break;
 
                 case LOCALE_SPAN:
diff --git a/core/java/android/text/style/EasyEditSpan.java b/core/java/android/text/style/EasyEditSpan.java
index 2feb719..03b4f60 100644
--- a/core/java/android/text/style/EasyEditSpan.java
+++ b/core/java/android/text/style/EasyEditSpan.java
@@ -16,6 +16,7 @@
 
 package android.text.style;
 
+import android.app.PendingIntent;
 import android.os.Parcel;
 import android.text.ParcelableSpan;
 import android.text.TextUtils;
@@ -25,12 +26,62 @@
  * Provides an easy way to edit a portion of text.
  * <p>
  * The {@link TextView} uses this span to allow the user to delete a chuck of text in one click.
- * the text. {@link TextView} removes this span as soon as the text is edited, or the cursor moves.
+ * <p>
+ * {@link TextView} removes the span when the user deletes the whole text or modifies it.
+ * <p>
+ * This span can be also used to receive notification when the user deletes or modifies the text;
  */
 public class EasyEditSpan implements ParcelableSpan {
 
+    /**
+     * The extra key field in the pending intent that describes how the text changed.
+     *
+     * @see #TEXT_DELETED
+     * @see #TEXT_MODIFIED
+     * @see #getPendingIntent()
+     */
+    public static final String EXTRA_TEXT_CHANGED_TYPE =
+            "android.text.style.EXTRA_TEXT_CHANGED_TYPE";
+
+    /**
+     * The value of {@link #EXTRA_TEXT_CHANGED_TYPE} when the text wrapped by this span is deleted.
+     */
+    public static final int TEXT_DELETED = 1;
+
+    /**
+     * The value of {@link #EXTRA_TEXT_CHANGED_TYPE} when the text wrapped by this span is modified.
+     */
+    public static final int TEXT_MODIFIED = 2;
+
+    private final PendingIntent mPendingIntent;
+
+    private boolean mDeleteEnabled;
+
+    /**
+     * Creates the span. No intent is sent when the wrapped text is modified or
+     * deleted.
+     */
     public EasyEditSpan() {
-        // Empty
+        mPendingIntent = null;
+        mDeleteEnabled = true;
+    }
+
+    /**
+     * @param pendingIntent The intent will be sent when the wrapped text is deleted or modified.
+     *                      When the pending intent is sent, {@link #EXTRA_TEXT_CHANGED_TYPE} is
+     *                      added in the intent to describe how the text changed.
+     */
+    public EasyEditSpan(PendingIntent pendingIntent) {
+        mPendingIntent = pendingIntent;
+        mDeleteEnabled = true;
+    }
+
+    /**
+     * Constructor called from {@link TextUtils} to restore the span.
+     */
+    public EasyEditSpan(Parcel source) {
+        mPendingIntent = source.readParcelable(null);
+        mDeleteEnabled = (source.readByte() == 1);
     }
 
     @Override
@@ -40,11 +91,39 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        // Empty
+        dest.writeParcelable(mPendingIntent, 0);
+        dest.writeByte((byte) (mDeleteEnabled ? 1 : 0));
     }
 
     @Override
     public int getSpanTypeId() {
         return TextUtils.EASY_EDIT_SPAN;
     }
+
+    /**
+     * @return True if the {@link TextView} should offer the ability to delete the text.
+     *
+     * @hide
+     */
+    public boolean isDeleteEnabled() {
+        return mDeleteEnabled;
+    }
+
+    /**
+     * Enables or disables the deletion of the text.
+     *
+     * @hide
+     */
+    public void setDeleteEnabled(boolean value) {
+        mDeleteEnabled = value;
+    }
+
+    /**
+     * @return the pending intent to send when the wrapped text is deleted or modified.
+     *
+     * @hide
+     */
+    public PendingIntent getPendingIntent() {
+        return mPendingIntent;
+    }
 }
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 862e2c8..dc305a5 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -20,6 +20,8 @@
 import com.android.internal.widget.EditableInputConnection;
 
 import android.R;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
 import android.content.ClipData;
 import android.content.ClipData.Item;
 import android.content.Context;
@@ -1890,10 +1892,23 @@
 
                 // Make sure there is only at most one EasyEditSpan in the text
                 if (mPopupWindow.mEasyEditSpan != null) {
-                    text.removeSpan(mPopupWindow.mEasyEditSpan);
+                    mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
                 }
 
                 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
+                mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
+                    @Override
+                    public void onDeleteClick(EasyEditSpan span) {
+                        Editable editable = (Editable) mTextView.getText();
+                        int start = editable.getSpanStart(span);
+                        int end = editable.getSpanEnd(span);
+                        if (start >= 0 && end >= 0) {
+                            sendNotification(EasyEditSpan.TEXT_DELETED, span);
+                            mTextView.deleteText_internal(start, end);
+                        }
+                        editable.removeSpan(span);
+                    }
+                });
 
                 if (mTextView.getWindowVisibility() != View.VISIBLE) {
                     // The window is not visible yet, ignore the text change.
@@ -1927,8 +1942,10 @@
         @Override
         public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
                 int newStart, int newEnd) {
-            if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
-                text.removeSpan(mPopupWindow.mEasyEditSpan);
+            if (mPopupWindow != null && span instanceof EasyEditSpan) {
+                EasyEditSpan easyEditSpan = (EasyEditSpan) span;
+                sendNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
+                text.removeSpan(easyEditSpan);
             }
         }
 
@@ -1938,6 +1955,31 @@
                 mTextView.removeCallbacks(mHidePopup);
             }
         }
+
+        private void sendNotification(int textChangedType, EasyEditSpan span) {
+            try {
+                PendingIntent pendingIntent = span.getPendingIntent();
+                if (pendingIntent != null) {
+                    Intent intent = new Intent();
+                    intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
+                    pendingIntent.send(mTextView.getContext(), 0, intent);
+                }
+            } catch (CanceledException e) {
+                // This should not happen, as we should try to send the intent only once.
+                Log.w(TAG, "PendingIntent for notification cannot be sent", e);
+            }
+        }
+    }
+
+    /**
+     * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
+     */
+    private interface EasyEditDeleteListener {
+
+        /**
+         * Clicks the delete pop-up.
+         */
+        void onDeleteClick(EasyEditSpan span);
     }
 
     /**
@@ -1950,6 +1992,7 @@
                 com.android.internal.R.layout.text_edit_action_popup_text;
         private TextView mDeleteTextView;
         private EasyEditSpan mEasyEditSpan;
+        private EasyEditDeleteListener mOnDeleteListener;
 
         @Override
         protected void createPopupWindow() {
@@ -1984,19 +2027,29 @@
             mEasyEditSpan = easyEditSpan;
         }
 
+        private void setOnDeleteListener(EasyEditDeleteListener listener) {
+            mOnDeleteListener = listener;
+        }
+
         @Override
         public void onClick(View view) {
-            if (view == mDeleteTextView) {
-                Editable editable = (Editable) mTextView.getText();
-                int start = editable.getSpanStart(mEasyEditSpan);
-                int end = editable.getSpanEnd(mEasyEditSpan);
-                if (start >= 0 && end >= 0) {
-                    mTextView.deleteText_internal(start, end);
-                }
+            if (view == mDeleteTextView
+                    && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
+                    && mOnDeleteListener != null) {
+                mOnDeleteListener.onDeleteClick(mEasyEditSpan);
             }
         }
 
         @Override
+        public void hide() {
+            if (mEasyEditSpan != null) {
+                mEasyEditSpan.setDeleteEnabled(false);
+            }
+            mOnDeleteListener = null;
+            super.hide();
+        }
+
+        @Override
         protected int getTextOffset() {
             // Place the pop-up at the end of the span
             Editable editable = (Editable) mTextView.getText();