In-depth article on Android touch events handling

Sometimes you need to intercept touches for a given ViewGroup, to temporarily change or disable it’s behavior. For example, recently I had to do this to temporarily intercept events for a RecyclerView during a tutorial, instructing the user how to select a particular item.

In this case, I could simply add an overlaid Layout on top of everything, but to keep as close to the real thing as possible, I decided to intercept the touch events at the RecyclerView level. The problem is, how to detect if the touch event needs to be intercepted or not. Two possible solutions came to my mind:

  • Intercept (or somehow disable) the events on each individual item, except on the one we’re interested on receiving it. For me it’s a bit messy to have this logic scattered.
  • Intercept the events on the RecyclerView, and check if the event belongs to the child, to allow further processing.

So, I decided to go for the second solution. To do that, I investigated a bit more in depth how touch events are handled in Android. This a really interesting article on how this happens: Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch). Also, it is really useful to see the real code on how a ViewGroup handles this. Even if I could download Android source code from the Android Open Source Project web site, I didn’t really felt like it. So I found this copy online.

The interesting part for us is ViewGroup#dispatchTouchEvent(MotionEvent), and specifically in the loop where it iterates all the child and checks if the touch event is within the bounds of this child.

Based on this, I extracted the relevant code, which boils down to retrieving the hit rectangle of the child we want to allow processing the event, and checking it against the touch target. This class has to be overridden and the getAllowedChildView() method implemented, returning the child we’re interested in allowing the touch events go through.

public class CustomRecyclerView extends RecyclerView {

    private static final String LOG_MARKER = CustomRecyclerView.class.getName();

    private boolean scrollEnabled = true;

    public CustomRecyclerView(Context context) {
        super(context);
    }

    public CustomRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public void setEnabledRecycleScroll(final boolean enable) {

        if (scrollEnabled == enable) {
            Logger.getInstance().debug(LOG_MARKER, "RecyclerView scrolling is already " +
                    (enable ? "enabled" : "disabled") + ", skipping");
            return;
        }

        scrollEnabled = enable;

        if (!enable) {
            Logger.getInstance().debug(LOG_MARKER, "Disabling RecyclerView scrolling");
            addOnItemTouchListener(disablerListener); // disables scrolling
        } else {
            Logger.getInstance().debug(LOG_MARKER, "Enabling RecyclerView scrolling");
            removeOnItemTouchListener(disablerListener); // scrolling is enabled again
        }

    }

    // Override this method to return the child we're interested in allowing touch events
    // to go through
    abstract protected View getAllowedChildView();

    private RecyclerView.OnItemTouchListener disablerListener =
            new RecyclerView.OnItemTouchListener() {
        @Override
        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {

            // Inspired by ViewGroup#dispatchTouchEvent(MotionEvent). See
            // http://codetheory.in/understanding-android-input-touch-events/ and
            // http://www.netmite.com/android/mydroid/frameworks/base/core/java/android/view/ViewGroup.java
            final int action = ev.getAction();
            if (action == MotionEvent.ACTION_DOWN) {
                final float scrolledX = ev.getX() + CustomRecyclerView.this.getScrollX();
                final float scrolledY = ev.getY() + CustomRecyclerView.this.getScrollY();
                final Rect frame = new Rect();
                View allowedChildView = getAllowedChildView();
                if (allowedChildView != null) {
                    allowedChildView.getHitRect(frame);
                    if (frame.contains((int) scrolledX, (int) scrolledY)) {
                        // Do not intercept the touch events for this child
                        return false;
                    }
                }
            }

            return true;
        }

        @Override
        public void onTouchEvent(RecyclerView rv, MotionEvent e) {
        }
    };

}

 

Disabling logs on Android using ProGuard

A quick way of disabling the logs for release builds is using ProGuard to take care of it. To do it in our current project we’ve created two ProGuard configurations, the one that applies for all the builds and the one that only applies for the release build.

Then, we can configure the build.gradle file like this:

apply plugin: 'com.android.application'

// snip...

android {

    // snip...

    buildTypes {
        release {

            // snip...

            // Enable ProGuard
            minifyEnabled true

            // Common release options
            zipAlignEnabled true
            debuggable false
            jniDebuggable false

            // Notice that the default ProGuard file (SDK-provided) also enables optimization
            // Here we also include a third file which disables the logging (see below)
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'proguard-rules-disable-logging.pro'
        }

        debug {
            // We enable ProGuard also for debug builds
            minifyEnabled true

            // Notice that the default ProGuard file (SDK-provided) differs from the release one
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

The file to disable logging is as simple as that:

##
## Disable logging
##

# Disable Android logging
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

# This gets rid of System.out.println() and System.out.print()
# WARNING: if you're using this functions for other PrintStreams in your app, this can break things!
-assumenosideeffects class java.io.PrintStream {
    public void println(...);
    public void print(...);
}

Android compatibility with 32-bit libraries on a 64-bit device

Recently I run into a problem with an app we’re working on: suddenly it stopped to work on a Samsung Galaxy S6 Edge. It was still working on all the other, older devices. The first idea was: the app is using a JNI library, so it’s a 64-bit compatibility problem. And bingo! This was the reason… but in an unexpected way.

The strange thing was that the app used to work on this device, and with this library (SQLcipher, to keep data secure on rest). So we started to see what changed: the OS recently got an update, to 5.1.1. Maybe there was a compatibility provision on the OS, but after this OS upgrade this had been disabled.

After some investigation, I found the following post in stackoverflow.com: Use 32-bit jni libraries on 64-bit android. Basically the answer stated that as far as you use System.loadLibrary() to load your JNI libraries, a 64-bit OS will try to load the 32-bit version of your library if it doesn’t find a 64-bit version. But this was not happening any more!

After some further investigation, I found that recently we added a module provided by a third party, which had a dependency to another library that had compatibility to both 32 and 64-bit. I’ve added ** next to each of the offending files.

$ unzip -l myapp-development-debug.apk
...
  186244 09-10-15 11:31 lib/armeabi/libdatabase_sqlcipher.so
 2478904 09-10-15 11:31 lib/armeabi/libsqlcipher_android.so
  390456 09-10-15 11:31 lib/armeabi/libstlport_shared.so
  182156 09-10-15 11:31 lib/armeabi-v7a/libdatabase_sqlcipher.so
 2421584 09-10-15 11:31 lib/armeabi-v7a/libsqlcipher_android.so
  374076 09-10-15 11:31 lib/armeabi-v7a/libstlport_shared.so
 1580868 09-10-15 11:31 lib/x86/libdatabase_sqlcipher.so
 3897372 09-10-15 11:31 lib/x86/libsqlcipher_android.so
  563148 09-10-15 11:31 lib/x86/libstlport_shared.so
   34224 09-09-15 13:29 lib/arm64-v8a/libpl_droidsonroids_gif.so **
    9624 09-09-15 13:29 lib/arm64-v8a/libpl_droidsonroids_gif_surface.so **
   38064 09-09-15 13:29 lib/armeabi/libpl_droidsonroids_gif.so
   17572 09-09-15 13:29 lib/armeabi/libpl_droidsonroids_gif_surface.so
   29884 09-09-15 13:29 lib/armeabi-v7a/libpl_droidsonroids_gif.so
   13488 09-09-15 13:29 lib/armeabi-v7a/libpl_droidsonroids_gif_surface.so
   73488 09-09-15 13:29 lib/mips/libpl_droidsonroids_gif.so **
   71132 09-09-15 13:29 lib/mips/libpl_droidsonroids_gif_surface.so **
   41544 09-09-15 13:29 lib/mips64/libpl_droidsonroids_gif.so **
   10360 09-09-15 13:29 lib/mips64/libpl_droidsonroids_gif_surface.so **
   33868 09-09-15 13:29 lib/x86/libpl_droidsonroids_gif.so
    9280 09-09-15 13:29 lib/x86/libpl_droidsonroids_gif_surface.so
   34416 09-09-15 13:29 lib/x86_64/libpl_droidsonroids_gif.so **
    9816 09-09-15 13:29 lib/x86_64/libpl_droidsonroids_gif_surface.so **
   95397 09-14-15 15:31 META-INF/MANIFEST.MF
   95426 09-14-15 15:31 META-INF/CERT.SF
    1318 09-14-15 15:31 META-INF/CERT.RSA
 --------               -------
 44599565               947 files

 
By simply using unzip tool we can clearly see the directories for the arm64-v8a and x86_64 architectures, and also the mips and mips64. In this directories the only present library are libpl_droidsonroids_gif and libpl_droidsonroids_gif_surface.

A quick and dirty solution to get rid of this libraries is to manually use “zip -d” to remove the unwanted architectures, like this:

zip -d myapp-app-development-debug.apk "lib/x86_64/*"
zip -d myapp-app-development-debug.apk "lib/mips64/*"
zip -d myapp-app-development-debug.apk "lib/mips/*"
zip -d myapp-app-development-debug.apk "lib/arm64-v8a/*"

 
Of course, this is useful to test that this is the cause of the crash, but not something you want to do on each build.

So, to automate this steps in Gradle we can use packagingOptions.exclude in the android section, like this (parts omitted):

android {
    compileSdkVersion Integer.parseInt(project.ANDROID_COMPILE_SDK_VERSION)
    buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION

    defaultConfig {
        ...
    }

    ...

    packagingOptions {
        exclude 'META-INF/LICENSE.txt'

        exclude 'lib/arm64-v8a/libpl_droidsonroids_gif.so'
        exclude 'lib/arm64-v8a/libpl_droidsonroids_gif_surface.so'
        exclude 'lib/x86_64/libpl_droidsonroids_gif.so'
        exclude 'lib/x86_64/libpl_droidsonroids_gif_surface.so'
        exclude 'lib/mips/libpl_droidsonroids_gif.so'
        exclude 'lib/mips/libpl_droidsonroids_gif_surface.so'
        exclude 'lib/mips64/libpl_droidsonroids_gif.so'
        exclude 'lib/mips64/libpl_droidsonroids_gif_surface.so'
    }

}

 
Notice that exclude doesn’t allow globs or wildcards, and we have to specify each file manually.

Language change detection in Android Jellybean 4.2+

In the current project I’m working on we had to detect system language change. We wanted to rely on Android configuration change mechanism, that handles this for us, but we had to do some additional state modifications beside UI.

So, the easy way is to detect configuration changes and handle them, following Handling Runtime Changes. Ok, so we decided to add a android:configChanges attribute with a value “locale” to the activity and add override onConfigurationChanged in the activity… and surprisingly nothing happened.

After some investigation we found that the problem happens on API 17+ and that the reason is that for this API, both “locale” and “layoutDirection” has to be specified, as Android now generates both in response to a language change (similarly to what happened in API 13+ with “orientation” and “screenSize”). The original answer came from StackOverflow.

BTW at the end we didn’t use this solution. Instead we used a BroadcastReceiver for the intent ACTION_LOCALE_CHANGED. The reason is this way we allow Android normal mechanism to handle the Activity restart for us (handling the configuration change by ourselves sidesteps this) and at the same time detect the language change.

 

Showing a rotation-locked activity programatically in Android

Sometimes you need an activity showing in a locked orientation (i.e. no rotation allowed), but you need to decide which orientation should be the activity display dynamically by code. In my case, I had to implement a chroma-keyed camera app and the backgrounds where either landscape or portrait. When the user tapped the background to use I had to decide the orientation and lock the new camera activity to this orientation.

My first thought was using setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_x); on the onCreate() in the activity, but I found that was not reliable. In some cases the screen didn’t lock and rotated freely. I was doing my tests in a Honeycomb Galaxy Tab, so I’m not sure if in 4.x this method should work.

Finally, the method I used and that worked flawlessly was declaring two different activities in the manifest, using android:screenOrientation attribute:

<activity
 android:name="com.xrubio.retrats.activity.ActivityCameraLandscape"
 android:screenOrientation="landscape"
 android:theme="@style/AppTheme.NoActionBar" />
<activity
 android:name="com.xrubio.retrats.activity.ActivityCameraPortrait"
 android:screenOrientation="portrait"
 android:theme="@style/AppTheme.NoActionBar" />

The trick is not duplicate the whole class code, but to put all the logic in a base class ActivityCameraBase and derive two subclasses with the names ActivityCameraLandscape and ActivityCameraLandscape. Also, there will be a single layout file, for both orientations.

Activities detail

package com.xrubio.retrats.activity;

// imports here

// This class is used to be able to have two different orientations,
// locked at manifest level, so we're able to select which orientation
// we want the camera be locked at programatically, without having to
// reimplement everything again.
public class ActivityCameraBase extends Activity {

  @Override
  public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_camera);

    // remaining initialization stuff...
  }

  // remaining activity logic...
}
package com.xrubio.retrats.activity;

public class ActivityCameraLandscape extends ActivityCameraBase {
  // Look, ma, the class is empty!
}
package com.xrubio.retrats.activity;

public class ActivityCameraPortrait extends ActivityCameraBase {
  // Look, ma, the class is empty!
}

Notice, however, that you can have two different layouts if you need your layout to be different in landscape and portrait, but keep the logic the same. In this case you will put your portrait layout in the regular layout directory, and your landscape layout in the layout-land directory, using the same name.

Different layouts detail