add SafeViewPager to guard ViewPager crash

Phillip Davis created

Android's ViewPager caches the active pointer id from ACTION_DOWN and
later calls findPointerIndex(mActivePointerId) on MOVE events. When
the OS delivers a MOVE whose pointer set no longer contains that id
(a known multi-touch race), findPointerIndex returns -1 and getX(-1)
throws IllegalArgumentException from native code, crashing the app:

    java.lang.IllegalArgumentException: invalid pointerIndex -1 for
    MotionEvent { action=MOVE, ... }
      at android.view.MotionEvent.nativeGetAxisValue(Native Method)
      at android.view.MotionEvent.getX(MotionEvent.java:2655)
      at androidx.viewpager.widget.ViewPager.onInterceptTouchEvent(ViewPager.java:2087)

SafeViewPager wraps onInterceptTouchEvent and onTouchEvent in a
try/catch and returns false on the exception, letting children handle
the event and letting the pager recover on the next clean DOWN.
Subsequent commits route the three ViewPager instances in the app
through this class.

A couple of places have implemented basically this exact workaround,
e.g., https://github.com/signalapp/Signal-Android/blob/main/app/src/main/java/org/thoughtcrime/securesms/components/viewpager/HackyViewPager.java

Change summary

src/cheogram/java/com/cheogram/android/SafeViewPager.java         | 39 +
src/cheogram/java/com/cheogram/android/WebviewAwareViewPager.java |  2 
src/cheogram/res/layout/activity_welcome.xml                      |  4 
src/main/res/layout/activity_start_conversation.xml               |  4 
4 files changed, 44 insertions(+), 5 deletions(-)

Detailed changes

src/cheogram/java/com/cheogram/android/SafeViewPager.java 🔗

@@ -0,0 +1,39 @@
+package com.cheogram.android;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+// Works around a long-standing Android bug where ViewPager's onInterceptTouchEvent /
+// onTouchEvent can trip over a stale mActivePointerId: a MOVE arrives whose pointer
+// set no longer contains the id cached from the earlier DOWN, so findPointerIndex
+// returns -1 and getX(-1) throws IllegalArgumentException from native code. Returning
+// false means "not intercepting / not consuming", which lets children handle the
+// event and lets the pager recover on the next clean DOWN.
+public class SafeViewPager extends androidx.viewpager.widget.ViewPager {
+	public SafeViewPager(Context context) {
+		super(context);
+	}
+
+	public SafeViewPager(Context context, AttributeSet attrs) {
+		super(context, attrs);
+	}
+
+	@Override
+	public boolean onInterceptTouchEvent(MotionEvent ev) {
+		try {
+			return super.onInterceptTouchEvent(ev);
+		} catch (IllegalArgumentException e) {
+			return false;
+		}
+	}
+
+	@Override
+	public boolean onTouchEvent(MotionEvent ev) {
+		try {
+			return super.onTouchEvent(ev);
+		} catch (IllegalArgumentException e) {
+			return false;
+		}
+	}
+}

src/cheogram/java/com/cheogram/android/WebviewAwareViewPager.java 🔗

@@ -5,7 +5,7 @@ import android.util.AttributeSet;
 import android.view.View;
 import android.webkit.WebView;
 
-public class WebviewAwareViewPager extends androidx.viewpager.widget.ViewPager {
+public class WebviewAwareViewPager extends SafeViewPager {
 	public WebviewAwareViewPager(Context context) {
 		super(context);
 	}

src/cheogram/res/layout/activity_welcome.xml 🔗

@@ -5,7 +5,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <androidx.viewpager.widget.ViewPager
+    <com.cheogram.android.SafeViewPager
         android:id="@+id/slideshow_pager"
         android:layout_width="0dp"
         android:layout_height="0dp"
@@ -274,7 +274,7 @@
         </ScrollView>
     </LinearLayout>
 
-    </androidx.viewpager.widget.ViewPager>
+    </com.cheogram.android.SafeViewPager>
 
    <androidx.constraintlayout.widget.Guideline
         android:id="@+id/gl_SlidePage"

src/main/res/layout/activity_start_conversation.xml 🔗

@@ -30,14 +30,14 @@
         </com.google.android.material.appbar.AppBarLayout>
 
 
-        <androidx.viewpager.widget.ViewPager
+        <com.cheogram.android.SafeViewPager
             android:id="@+id/start_conversation_view_pager"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_below="@id/app_bar_layout">
 
 
-        </androidx.viewpager.widget.ViewPager>
+        </com.cheogram.android.SafeViewPager>
 
         <com.leinardi.android.speeddial.SpeedDialOverlayLayout
             android:id="@+id/overlay"