Commit 879d892a authored by yfriedman's avatar yfriedman Committed by Commit bot

Replace WindowCallbackWrapper with a generated instance.

Rather than having to manually define each function in Window.Callback
and forward it along, instead use reflection to automatically forward
all calls that aren't interesting.

BUG=487378

Review URL: https://codereview.chromium.org/1139913005

Cr-Commit-Position: refs/heads/master@{#330015}
parent 34f8e9ed
......@@ -11,6 +11,10 @@ import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Window;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Basic application functionality that should be shared among all browser applications.
*/
......@@ -30,66 +34,94 @@ public class BaseChromiumApplication extends Application {
private ObserverList<WindowFocusChangedListener> mWindowFocusListeners =
new ObserverList<WindowFocusChangedListener>();
/**
* Intercepts calls to an existing Window.Callback. Most invocations are passed on directly
* to the composed Window.Callback but enables intercepting/manipulating others.
*
* This is used to relay window focus changes throughout the app and remedy a bug in the
* appcompat library.
*/
private class WindowCallbackProxy implements InvocationHandler {
private final Window.Callback mCallback;
private final Activity mActivity;
public WindowCallbackProxy(Activity activity, Window.Callback callback) {
mCallback = callback;
mActivity = activity;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("onWindowFocusChanged") && args.length == 1
&& args[0] instanceof Boolean) {
onWindowFocusChanged((boolean) args[0]);
return null;
} else if (method.getName().equals("dispatchKeyEvent") && args.length == 1
&& args[0] instanceof KeyEvent) {
return dispatchKeyEvent((KeyEvent) args[0]);
} else {
return method.invoke(mCallback, args);
}
}
public void onWindowFocusChanged(boolean hasFocus) {
mCallback.onWindowFocusChanged(hasFocus);
for (WindowFocusChangedListener listener : mWindowFocusListeners) {
listener.onWindowFocusChanged(mActivity, hasFocus);
}
}
public boolean dispatchKeyEvent(KeyEvent event) {
// TODO(aurimas): remove this once AppCompatDelegateImpl no longer steals
// KEYCODE_MENU. (see b/20529185)
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU && mActivity.dispatchKeyEvent(event)) {
return true;
}
return mCallback.dispatchKeyEvent(event);
}
}
@Override
public void onCreate() {
super.onCreate();
ApplicationStatus.initialize(this);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
Window.Callback callback = activity.getWindow().getCallback();
activity.getWindow().setCallback(new WindowCallbackWrapper(callback) {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
for (WindowFocusChangedListener listener : mWindowFocusListeners) {
listener.onWindowFocusChanged(activity, hasFocus);
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// TODO(aurimas): remove this once AppCompatDelegateImpl no longer steals
// KEYCODE_MENU. (see b/20529185)
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU
&& activity.dispatchKeyEvent(event)) {
return true;
}
return super.dispatchKeyEvent(event);
}
});
activity.getWindow().setCallback((Window.Callback) Proxy.newProxyInstance(
Window.Callback.class.getClassLoader(), new Class[] {Window.Callback.class},
new WindowCallbackProxy(activity, callback)));
}
@Override
public void onActivityDestroyed(Activity activity) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
@Override
public void onActivityPaused(Activity activity) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
@Override
public void onActivityResumed(Activity activity) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
@Override
public void onActivityStarted(Activity activity) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
@Override
public void onActivityStopped(Activity activity) {
assert activity.getWindow().getCallback() instanceof WindowCallbackWrapper;
assert Proxy.isProxyClass(activity.getWindow().getCallback().getClass());
}
});
}
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import android.annotation.SuppressLint;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
/**
* A wrapper for a Window.Callback instance, allowing subclasses to listen to or override specific
* window messages.
*/
class WindowCallbackWrapper implements Window.Callback {
private final Window.Callback mCallback;
public WindowCallbackWrapper(Window.Callback callback) {
mCallback = callback;
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
return mCallback.dispatchGenericMotionEvent(event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return mCallback.dispatchKeyEvent(event);
}
@Override
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
return mCallback.dispatchKeyShortcutEvent(event);
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
return mCallback.dispatchPopulateAccessibilityEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return mCallback.dispatchTouchEvent(event);
}
@Override
public boolean dispatchTrackballEvent(MotionEvent event) {
return mCallback.dispatchTrackballEvent(event);
}
@Override
public void onActionModeFinished(ActionMode mode) {
mCallback.onActionModeFinished(mode);
}
@Override
public void onActionModeStarted(ActionMode mode) {
mCallback.onActionModeStarted(mode);
}
@Override
public void onAttachedToWindow() {
mCallback.onAttachedToWindow();
}
@Override
public void onContentChanged() {
mCallback.onContentChanged();
}
@Override
public boolean onCreatePanelMenu(int featureId, Menu menu) {
return mCallback.onCreatePanelMenu(featureId, menu);
}
@Override
public View onCreatePanelView(int featureId) {
return mCallback.onCreatePanelView(featureId);
}
@Override
@SuppressLint("MissingSuperCall")
public void onDetachedFromWindow() {
mCallback.onDetachedFromWindow();
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
return mCallback.onMenuItemSelected(featureId, item);
}
@Override
public boolean onMenuOpened(int featureId, Menu menu) {
return mCallback.onMenuOpened(featureId, menu);
}
@Override
public void onPanelClosed(int featureId, Menu menu) {
mCallback.onPanelClosed(featureId, menu);
}
@Override
public boolean onPreparePanel(int featureId, View view, Menu menu) {
return mCallback.onPreparePanel(featureId, view, menu);
}
@Override
public boolean onSearchRequested() {
return mCallback.onSearchRequested();
}
@Override
public void onWindowAttributesChanged(LayoutParams attrs) {
mCallback.onWindowAttributesChanged(attrs);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
mCallback.onWindowFocusChanged(hasFocus);
}
@Override
public ActionMode onWindowStartingActionMode(Callback callback) {
return mCallback.onWindowStartingActionMode(callback);
}
public void onWindowDismissed() {
// TODO(benm): implement me.
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.Activity;
import android.view.KeyEvent;
import junit.framework.Assert;
import org.chromium.base.BaseChromiumApplication.WindowFocusChangedListener;
import org.chromium.testing.local.LocalRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.util.ActivityController;
/** Unit tests for {@link BaseChromiumApplication}. */
@RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = BaseChromiumApplication.class,
shadows = {BaseChromiumApplicationTest.TrackingShadowActivity.class})
public class BaseChromiumApplicationTest {
@Implements(Activity.class)
public static class TrackingShadowActivity extends ShadowActivity {
private int mWindowFocusCalls;
private int mDispatchKeyEventCalls;
private boolean mReturnValueForKeyDispatch;
@Implementation
public void onWindowFocusChanged(@SuppressWarnings("unused") boolean hasFocus) {
mWindowFocusCalls++;
}
@Implementation
public boolean dispatchKeyEvent(@SuppressWarnings("unused") KeyEvent event) {
mDispatchKeyEventCalls++;
return mReturnValueForKeyDispatch;
}
}
@Test
public void testWindowsFocusChanged() throws Exception {
BaseChromiumApplication app = (BaseChromiumApplication) Robolectric.application;
WindowFocusChangedListener mock = mock(WindowFocusChangedListener.class);
app.registerWindowFocusChangedListener(mock);
ActivityController<Activity> controller =
Robolectric.buildActivity(Activity.class).create().start().visible();
TrackingShadowActivity shadow =
(TrackingShadowActivity) Robolectric.shadowOf(controller.get());
controller.get().getWindow().getCallback().onWindowFocusChanged(true);
// Assert that listeners were notified.
verify(mock).onWindowFocusChanged(controller.get(), true);
// Also ensure that the original activity is forwarded the notification.
Assert.assertEquals(1, shadow.mWindowFocusCalls);
}
@Test
public void testDispatchKeyEvent() throws Exception {
ActivityController<Activity> controller =
Robolectric.buildActivity(Activity.class).create().start().visible();
TrackingShadowActivity shadow =
(TrackingShadowActivity) Robolectric.shadowOf(controller.get());
final KeyEvent menuKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU);
// Ensure that key events are forwarded.
Assert.assertFalse(controller.get().getWindow().getCallback().dispatchKeyEvent(menuKey));
// This gets called twice - once to see if the activity is swallowing it, and again to
// dispatch it.
Assert.assertEquals(2, shadow.mDispatchKeyEventCalls);
// Ensure that our activity can swallow the event.
shadow.mReturnValueForKeyDispatch = true;
Assert.assertTrue(controller.get().getWindow().getCallback().dispatchKeyEvent(menuKey));
Assert.assertEquals(3, shadow.mDispatchKeyEventCalls);
// A non-enter key only dispatches once.
Assert.assertTrue(controller.get().getWindow().getCallback().dispatchKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)));
Assert.assertEquals(4, shadow.mDispatchKeyEventCalls);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment