Upgraded to AppCompat v22.1.0 and now onKeyDown and onKeyUp are not triggered when menu key is pressed
Asked Answered
S

2

16

I've just upgraded my app to use the newly released v22.1.0 AppCompat and now onKeyDown and onKeyUp are not triggered when menu key is pressed. The other keys correctly trigger onKeyDown and onKeyUp, but when i press the menu key nothing happens. If I downgrade to v22.0.0 everything returns to work properly.

How do I fix it?

Snipes answered 24/4, 2015 at 16:4 Comment(4)
Answering your own question? At the same moment?Terebinthine
Yes, I followed the advice of this article in the Help Center of Stack OverflowSnipes
It seems onKeyDown and onKeyUp events are now correctly triggered for menu key on AppCompat v22.2.0.Steinbok
As stated by Chris Banes the release v22.2 resolves the issue.Snipes
S
31

Update 23 August

This has been fixed again in the v23.0.0 of appcompat-v7 support library. Update to the last version to see this fixed.


Update 19 July

Unfortunately AppCompat v22.2.1 broke the onKeyDown and onKeyUp events again. I just updated AppCompatActivityMenuKeyInterceptor to support v22.1.x and also v22.2.1


Update 29 May

This has been fixed in the v22.2.0 of appcompat-v7 support library. Update to the last version to see this fixed.


Unfortunately AppCompat v22.1.0 intercepts the onKeyDown and onKeyUp events and does not propagate them when the menu key is pressed. The only possible solution involves using Reflection to intercept the onKeyDown and onKeyUp events before the AppCompat does.

Add this class to your project:

public class AppCompatActivityMenuKeyInterceptor {

    private static final String FIELD_NAME_DELEGATE = "mDelegate";
    private static final String FIELD_NAME_WINDOW = "mWindow";

    public static void intercept(AppCompatActivity appCompatActivity) {
        new AppCompatActivityMenuKeyInterceptor(appCompatActivity);
    }

    private AppCompatActivityMenuKeyInterceptor(AppCompatActivity activity) {
        try {
            Field mDelegateField = AppCompatActivity.class.getDeclaredField(FIELD_NAME_DELEGATE);
            mDelegateField.setAccessible(true);
            Object mDelegate = mDelegateField.get(activity);

            Class mDelegateClass = mDelegate.getClass().getSuperclass();
            Field mWindowField = null;

            while (mDelegateClass != null) {
                try {
                    mWindowField = mDelegateClass.getDeclaredField(FIELD_NAME_WINDOW);
                    break;
                } catch (NoSuchFieldException ignored) {
                }

                mDelegateClass = mDelegateClass.getSuperclass();
            }

            if (mWindowField == null)
                throw new NoSuchFieldException(FIELD_NAME_WINDOW);

            mWindowField.setAccessible(true);
            Window mWindow = (Window) mWindowField.get(mDelegate);

            Window.Callback mOriginalWindowCallback = mWindow.getCallback();
            mWindow.setCallback(new AppCompatWindowCallbackCustom(mOriginalWindowCallback, activity));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private class AppCompatWindowCallbackCustom extends WindowCallbackWrapper {

        private WeakReference<AppCompatActivity> mActivityWeak;

        public AppCompatWindowCallbackCustom(Window.Callback wrapped, AppCompatActivity appCompatActivity) {
            super(wrapped);

            mActivityWeak = new WeakReference<AppCompatActivity>(appCompatActivity);
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            final int keyCode = event.getKeyCode();

            AppCompatActivity appCompatActivity = mActivityWeak.get();

            if (appCompatActivity != null && keyCode == KeyEvent.KEYCODE_MENU) {
                if (appCompatActivity.dispatchKeyEvent(event))
                    return true;
            }

            return super.dispatchKeyEvent(event);
        }
    }
}

Call AppCompatActivityMenuKeyInterceptor.intercept(this) in the onCreate of your activity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Initialize the interceptor
        AppCompatActivityMenuKeyInterceptor.intercept(this);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // Now onKeyDown is called also for KEYCODE_MENU
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            //do your stuff

            //return false if you want to propagate the
            //KeyEvent to AppCompat, return true otherwise
            return false;
        }

        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        // Now onKeyUp is called also for KEYCODE_MENU
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            //do your stuff

            //return false if you want to propagate the
            //KeyEvent to AppCompat, return true otherwise
            return false;
        }

        return super.onKeyUp(keyCode, event);
    }
}

If you use ProGuard or DexGuard add these rules to your configuration:

-keepclassmembers class android.support.v7.app.AppCompatActivity {
    private android.support.v7.app.AppCompatDelegate mDelegate;
}

-keepclassmembers class android.support.v7.app.AppCompatDelegateImplBase {
    final android.view.Window mWindow;
}

Now your activity can receive onKeyDown and onKeyUp event also for the menu key.

Snipes answered 24/4, 2015 at 16:4 Comment(4)
I ran into a problem with this after obfuscating my code. Keeping the support library seemed to help. Please comment on this if this seems wrong, but I added to my proguard-rules.pro -keep class android.support.v7.** { *; } -keep interface android.support.v7.** { *; }Herbage
@DaiwikDaarun Yes your rules are correct, but you keep all the support libraries. If you want a specific rules you can see the updated answer. Thanks for pointing that outSnipes
Unfortunately this issue and all the updates are specifically for KEYCODE_MENU, not for KEYCODE_BACK.Snipes
Excuse me sir, and for those who are not using AppCompat? I'm not being successful capturing KeyUp and KeyDown. Some advice would be very appreciated. Thank you.Neurosurgery
S
1

Instead of onKeyUp() or onKeyDown(), one can simply use dispatchKeyEvent(). Look at the following code from android-developers.blogspot.com.

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && event.getRepeatCount() == 0) {

            // Tell the framework to start tracking this event.
            getKeyDispatcherState().startTracking(event, this);
            return true;

        } else if (event.getAction() == KeyEvent.ACTION_UP) {
            getKeyDispatcherState().handleUpEvent(event);
            if (event.isTracking() && !event.isCanceled()) {
                // DO BACK ACTION HERE
                return true;
            }
        }
        return super.dispatchKeyEvent(event);
    } else {
        return super.dispatchKeyEvent(event);
    }
}
Scapegoat answered 20/10, 2015 at 9:38 Comment(4)
Unfortunately your solution doesn't work. The dispatchKeyEvent is not propagated when the menu key is pressed while using the v7 support library v22.1.0, v22.1.1 and v22.2.1. It is a known bug and it was fixed in the newest versions of v7 support library.Snipes
It means that hardware menu key press does not even work on 22.0.1Scapegoat
As you can see in the title this question is specifically for v22.1.0 and higher (v22.1.0, v22.1.1 and v22.2.1)Snipes
Worked great for me on latest SDK.Woodsman

© 2022 - 2024 — McMap. All rights reserved.