Some accessibility events not sent from touch explorer if apps misbehave.

1. The touch explorer is relying on the hover exit accessibility event to be sent
   from the app's view tree before sending the exploration end and last touch
   accessibility events. However, if the app is buggy and does not send the hover
   exit event, then the interaction ending events are never sent. Now there is a
   timeout in which we wait for the hover exit accessibility event before sending
   the gesture end and last touch accessibility events. Hence, we are making a
   best effort to have a consistent event stream.

2. Sneaking in the new nine patch for the border around the magnified region
   since the current one is engineering art.

bug:7233616

Change-Id: Ie64f23659c25ab914565d50537b9a82bdc6a44a0
diff --git a/core/res/res/drawable-hdpi/magnified_region_frame.9.png b/core/res/res/drawable-hdpi/magnified_region_frame.9.png
new file mode 100644
index 0000000..29bdc42
--- /dev/null
+++ b/core/res/res/drawable-hdpi/magnified_region_frame.9.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/magnified_region_frame.9.png b/core/res/res/drawable-mdpi/magnified_region_frame.9.png
new file mode 100644
index 0000000..a61cbea
--- /dev/null
+++ b/core/res/res/drawable-mdpi/magnified_region_frame.9.png
Binary files differ
diff --git a/core/res/res/drawable-nodpi/magnified_region_frame.9.png b/core/res/res/drawable-nodpi/magnified_region_frame.9.png
deleted file mode 100644
index 4cadefb..0000000
--- a/core/res/res/drawable-nodpi/magnified_region_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/magnified_region_frame.9.png b/core/res/res/drawable-xhdpi/magnified_region_frame.9.png
new file mode 100644
index 0000000..424b3d9
--- /dev/null
+++ b/core/res/res/drawable-xhdpi/magnified_region_frame.9.png
Binary files differ
diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java
index 542cc07..3e9bef0 100644
--- a/services/java/com/android/server/accessibility/TouchExplorer.java
+++ b/services/java/com/android/server/accessibility/TouchExplorer.java
@@ -102,6 +102,10 @@
     // The timeout after which we are no longer trying to detect a gesture.
     private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000;
 
+    // The timeout to send interaction end events in case we did not
+    // receive the expected hover exit event due to a misbehaving app.
+    private static final int SEND_INTERACTION_END_EVENTS_TIMEOUT = 200;
+
     // Temporary array for storing pointer IDs.
     private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT];
 
@@ -135,6 +139,9 @@
     // Command for delayed sending of a hover exit event.
     private final SendHoverDelayed mSendHoverExitDelayed;
 
+    // Command for delayed sending of interaction ending events.
+    private final SendInteractionEndEventsDelayed mSendInteractionEndEventsDelayed;
+
     // Command for delayed sending of a long press.
     private final PerformLongPressDelayed mPerformLongPressDelayed;
 
@@ -233,6 +240,7 @@
         mGestureLibrary.load();
         mSendHoverEnterDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_ENTER, true);
         mSendHoverExitDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_EXIT, false);
+        mSendInteractionEndEventsDelayed = new SendInteractionEndEventsDelayed();
         mDoubleTapDetector = new DoubleTapDetector();
         final float density = context.getResources().getDisplayMetrics().density;
         mScaledMinPointerDistanceToUseMiddleLocation =
@@ -278,6 +286,7 @@
         mSendHoverExitDelayed.remove();
         mPerformLongPressDelayed.remove();
         mExitGestureDetectionModeDelayed.remove();
+        mSendInteractionEndEventsDelayed.remove();
         // Reset the pointer trackers.
         mReceivedPointerTracker.clear();
         mInjectedPointerTracker.clear();
@@ -334,6 +343,7 @@
         // last hover exit event.
         if (mTouchExplorationGestureEnded
                 && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
+            mSendInteractionEndEventsDelayed.remove();
             mTouchExplorationGestureEnded = false;
             sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END);
         }
@@ -342,6 +352,7 @@
         // last hover exit and the touch exploration gesture end events.
         if (mTouchInteractionEnded
                 && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
+            mSendInteractionEndEventsDelayed.remove();
             mTouchInteractionEnded = false;
             sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
         }
@@ -416,6 +427,10 @@
                             mSendHoverExitDelayed.remove();
                         }
 
+                        if (mSendInteractionEndEventsDelayed.isPending()) {
+                            mSendInteractionEndEventsDelayed.forceSendAndRemove();
+                        }
+
                         mPerformLongPressDelayed.remove();
 
                         // If we have the first tap schedule a long press and break
@@ -873,6 +888,9 @@
             final int pointerIdBits = event.getPointerIdBits();
             mTouchExplorationGestureEnded = true;
             mTouchInteractionEnded = true;
+            if (!mSendInteractionEndEventsDelayed.isPending()) {
+                mSendInteractionEndEventsDelayed.post();
+            }
             sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags);
         }
     }
@@ -1484,10 +1502,16 @@
                 } else {
                     mTouchExplorationGestureEnded = true;
                     mTouchInteractionEnded = true;
+                    if (!mSendInteractionEndEventsDelayed.isPending()) {
+                        mSendInteractionEndEventsDelayed.post();
+                    }
                 }
             } else {
                 if (!mGestureStarted) {
                     mTouchInteractionEnded = true;
+                    if (!mSendInteractionEndEventsDelayed.isPending()) {
+                        mSendInteractionEndEventsDelayed.post();
+                    }
                 }
             }
             sendMotionEvent(mPrototype, mHoverAction, mPointerIdBits, mPolicyFlags);
@@ -1495,6 +1519,40 @@
         }
     }
 
+    private class SendInteractionEndEventsDelayed implements Runnable {
+
+        public void remove() {
+            mHandler.removeCallbacks(this);
+        }
+
+        public void post() {
+            mHandler.postDelayed(this, SEND_INTERACTION_END_EVENTS_TIMEOUT);
+        }
+
+        public boolean isPending() {
+            return mHandler.hasCallbacks(this);
+        }
+
+        public void forceSendAndRemove() {
+            if (isPending()) {
+                run();
+                remove();
+            }
+        }
+
+        @Override
+        public void run() {
+            if (mTouchExplorationGestureEnded) {
+                mTouchExplorationGestureEnded = false;
+                sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END);
+            }
+            if (mTouchInteractionEnded) {
+                mTouchInteractionEnded = false;
+                sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
+            }
+        }
+    }
+
     @Override
     public String toString() {
         return LOG_TAG;