Bug 1396951 - 1. Add and use HapticFeedbackDelegate; r?snorp
Instead of using `getLayerView()` to perform haptic feedback, this patch
adds a `HapticFeedbackDelegate`, which `GeckoApplication` implements to
call `performHapticFeedback()` on the active view. Also, use
HapticFeedbackDelegate elsewhere in the Fennec codebase where we want to
perform haptic feedback.
MozReview-Commit-ID: GAArA6yJFNF
--- a/mobile/android/app/src/main/res/values/arrays.xml
+++ b/mobile/android/app/src/main/res/values/arrays.xml
@@ -131,23 +131,16 @@
<item>@string/pref_update_autodownload_wifi</item>
<item>@string/pref_update_autodownload_disabled</item>
</string-array>
<string-array name="pref_update_autodownload_values">
<item>enabled</item>
<item>wifi</item>
<item>disabled</item>
</string-array>
- <!-- This value is similar to config_longPressVibePattern in android frameworks/base/core/res/res/values/config.xml-->
- <integer-array name="long_press_vibrate_msec">
- <item>0</item>
- <item>1</item>
- <item>20</item>
- <item>21</item>
- </integer-array>
<!-- browser.image_blocking -->
<string-array name="pref_browser_image_blocking_entries">
<item>@string/pref_tap_to_load_images_enabled</item>
<item>@string/pref_tap_to_load_images_data</item>
<item>@string/pref_tap_to_load_images_disabled2</item>
</string-array>
<string-array name="pref_browser_image_blocking_values">
<item>1</item> <!-- Always -->
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -44,16 +44,17 @@ import android.support.design.widget.Sna
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.MenuItemCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
@@ -693,17 +694,17 @@ public class BrowserApp extends GeckoApp
// being called. Hence we need to guard against the Activity being
// shut down (in which case trying to perform UI changes, such as showing
// fragments below, will crash).
return;
}
final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
final FragmentManager fragmentManager = getSupportFragmentManager();
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
+ GeckoAppShell.getHapticFeedbackDelegate().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
}
});
}
});
mBrowserToolbar.setTabHistoryController(tabHistoryController);
final String action = intent.getAction();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -52,17 +52,18 @@ import org.mozilla.gecko.util.GeckoBundl
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.PRNGFixes;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.File;
import java.lang.reflect.Method;
import java.util.UUID;
-public class GeckoApplication extends Application {
+public class GeckoApplication extends Application
+ implements HapticFeedbackDelegate {
private static final String LOG_TAG = "GeckoApplication";
private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
private boolean mInBackground;
private boolean mPausedGecko;
private boolean mIsInitialResume;
private LightweightTheme mLightweightTheme;
@@ -224,16 +225,17 @@ public class GeckoApplication extends Ap
sSessionUUID = UUID.randomUUID().toString();
GeckoActivityMonitor.getInstance().initialize(this);
MemoryMonitor.getInstance().init(this);
final Context context = getApplicationContext();
GeckoAppShell.setApplicationContext(context);
+ GeckoAppShell.setHapticFeedbackDelegate(this);
GeckoAppShell.setGeckoInterface(new GeckoAppShell.GeckoInterface() {
@Override
public boolean openUriExternal(final String targetURI, final String mimeType,
final String packageName, final String className,
final String action, final String title) {
// Default to showing prompt in private browsing to be safe.
return IntentHelper.openUriExternal(targetURI, mimeType, packageName,
className, action, title, true);
@@ -629,9 +631,18 @@ public class GeckoApplication extends Ap
new Rect(halfSize - sWidth,
halfSize - sHeight,
halfSize + sWidth,
halfSize + sHeight),
null);
return bitmap;
}
+
+ @Override // HapticFeedbackDelegate
+ public void performHapticFeedback(final int effect) {
+ final Activity currentActivity =
+ GeckoActivityMonitor.getInstance().getCurrentActivity();
+ if (currentActivity != null) {
+ currentActivity.getWindow().getDecorView().performHapticFeedback(effect);
+ }
+ }
}
--- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
@@ -12,16 +12,17 @@ import org.mozilla.gecko.util.ThreadUtil
import org.mozilla.gecko.widget.GeckoActionProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
+import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -254,34 +255,36 @@ public class GeckoMenu extends ListView
public void onClick(View view) {
handleMenuItemClick(menuItem);
}
});
((MenuItemActionBar) actionView).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (handleMenuItemLongClick(menuItem)) {
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
+ GeckoAppShell.getHapticFeedbackDelegate().performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS);
return true;
}
return false;
}
});
} else if (actionView instanceof MenuItemSwitcherLayout) {
((MenuItemSwitcherLayout) actionView).setMenuItemClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
handleMenuItemClick(menuItem);
}
});
((MenuItemSwitcherLayout) actionView).setMenuItemLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (handleMenuItemLongClick(menuItem)) {
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
+ GeckoAppShell.getHapticFeedbackDelegate().performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS);
return true;
}
return false;
}
});
}
return added;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -410,16 +410,17 @@ gvjar.sources += [geckoview_source_dir +
'gfx/PointUtils.java',
'gfx/RenderTask.java',
'gfx/StackScroller.java',
'gfx/SurfaceAllocator.java',
'gfx/SurfaceAllocatorService.java',
'gfx/SurfaceTextureListener.java',
'gfx/ViewTransform.java',
'gfx/VsyncSource.java',
+ 'HapticFeedbackDelegate.java',
'InputConnectionListener.java',
'InputMethods.java',
'media/AsyncCodec.java',
'media/AsyncCodecFactory.java',
'media/BaseHlsPlayer.java',
'media/Codec.java',
'media/CodecProxy.java',
'media/FormatParam.java',
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -376,17 +376,18 @@ public class GeckoAppShell
/* package */ static native void onLocationChanged(double latitude, double longitude,
double altitude, float accuracy,
float bearing, float speed, long time);
private static class DefaultListeners implements SensorEventListener,
LocationListener,
NotificationListener,
ScreenOrientationDelegate,
- WakeLockDelegate {
+ WakeLockDelegate,
+ HapticFeedbackDelegate {
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
private static int HalSensorAccuracyFor(int androidAccuracy) {
switch (androidAccuracy) {
case SensorManager.SENSOR_STATUS_UNRELIABLE:
return GeckoHalDefines.SENSOR_ACCURACY_UNRELIABLE;
@@ -556,23 +557,40 @@ public class GeckoAppShell
wl.acquire();
mWakeLocks.put(lock, wl);
} else if (state != WakeLockDelegate.STATE_LOCKED_FOREGROUND && wl != null) {
wl.release();
mWakeLocks.remove(lock);
}
}
+
+ @Override
+ public void performHapticFeedback(final int effect) {
+ final int[] pattern;
+ // Use default platform values.
+ if (effect == HapticFeedbackConstants.KEYBOARD_TAP) {
+ pattern = new int[] { 40 };
+ } else if (effect == HapticFeedbackConstants.LONG_PRESS) {
+ pattern = new int[] { 0, 1, 20, 21 };
+ } else if (effect == HapticFeedbackConstants.VIRTUAL_KEY) {
+ pattern = new int[] { 0, 10, 20, 30 };
+ } else {
+ return;
+ }
+ vibrateOnHapticFeedbackEnabled(pattern);
+ }
}
private static final DefaultListeners DEFAULT_LISTENERS = new DefaultListeners();
private static SensorEventListener sSensorListener = DEFAULT_LISTENERS;
private static LocationListener sLocationListener = DEFAULT_LISTENERS;
private static NotificationListener sNotificationListener = DEFAULT_LISTENERS;
private static WakeLockDelegate sWakeLockDelegate = DEFAULT_LISTENERS;
+ private static HapticFeedbackDelegate sHapticFeedbackDelegate = DEFAULT_LISTENERS;
/**
* A delegate for supporting the Screen Orientation API.
*/
private static ScreenOrientationDelegate sScreenOrientationDelegate = DEFAULT_LISTENERS;
public static SensorEventListener getSensorListener() {
return sSensorListener;
@@ -609,16 +627,24 @@ public class GeckoAppShell
public static WakeLockDelegate getWakeLockDelegate() {
return sWakeLockDelegate;
}
public void setWakeLockDelegate(final WakeLockDelegate delegate) {
sWakeLockDelegate = (delegate != null) ? delegate : DEFAULT_LISTENERS;
}
+ public static HapticFeedbackDelegate getHapticFeedbackDelegate() {
+ return sHapticFeedbackDelegate;
+ }
+
+ public static void setHapticFeedbackDelegate(final HapticFeedbackDelegate delegate) {
+ sHapticFeedbackDelegate = (delegate != null) ? delegate : DEFAULT_LISTENERS;
+ }
+
@WrapForJNI(calledFrom = "gecko")
private static void enableSensor(int aSensortype) {
final SensorManager sm = (SensorManager)
getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
switch (aSensortype) {
case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
if (gGameRotationVectorSensor == null) {
@@ -997,20 +1023,21 @@ public class GeckoAppShell
sScreenDepth = aScreenDepth;
}
@WrapForJNI(calledFrom = "gecko")
private static void performHapticFeedback(boolean aIsLongPress) {
// Don't perform haptic feedback if a vibration is currently playing,
// because the haptic feedback will nuke the vibration.
if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
- LayerView layerView = getLayerView();
- layerView.performHapticFeedback(aIsLongPress ?
- HapticFeedbackConstants.LONG_PRESS :
- HapticFeedbackConstants.VIRTUAL_KEY);
+ getHapticFeedbackDelegate().performHapticFeedback(
+ aIsLongPress ? HapticFeedbackConstants.LONG_PRESS
+ : HapticFeedbackConstants.VIRTUAL_KEY);
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
}
}
private static Vibrator vibrator() {
return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
}
// Helper method to convert integer array to long array.
@@ -1018,36 +1045,40 @@ public class GeckoAppShell
long[] output = new long[input.length];
for (int i = 0; i < input.length; i++) {
output[i] = input[i];
}
return output;
}
// Vibrate only if haptic feedback is enabled.
- public static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
+ private static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
if (Settings.System.getInt(getApplicationContext().getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
- vibrate(convertIntToLongArray(milliseconds), -1);
+ if (milliseconds.length == 1) {
+ vibrate(milliseconds[0]);
+ } else {
+ vibrate(convertIntToLongArray(milliseconds), -1);
+ }
}
}
@WrapForJNI(calledFrom = "gecko")
private static void vibrate(long milliseconds) {
sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
sVibrationMaybePlaying = true;
vibrator().vibrate(milliseconds);
}
@WrapForJNI(calledFrom = "gecko")
private static void vibrate(long[] pattern, int repeat) {
- // If pattern.length is even, the last element in the pattern is a
+ // If pattern.length is odd, the last element in the pattern is a
// meaningless delay, so don't include it in vibrationDuration.
long vibrationDuration = 0;
- int iterLen = pattern.length - (pattern.length % 2 == 0 ? 1 : 0);
+ int iterLen = pattern.length & ~1;
for (int i = 0; i < iterLen; i++) {
vibrationDuration += pattern[i];
}
sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
sVibrationMaybePlaying = true;
vibrator().vibrate(pattern, repeat);
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/HapticFeedbackDelegate.java
@@ -0,0 +1,20 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.view.HapticFeedbackConstants;
+
+/**
+ * A <code>HapticFeedbackDelegate</code> is responsible for performing haptic feedback.
+ */
+public interface HapticFeedbackDelegate {
+ /**
+ * Perform a haptic feedback effect. Called from the Gecko thread.
+ *
+ * @param effect Effect to perform from <code>android.view.HapticFeedbackConstants</code>.
+ */
+ void performHapticFeedback(int effect);
+}