Bug 1396951 - 1. Add and use HapticFeedbackDelegate; r=snorp
☠☠ backed out by 2cd3752963fc ☠ ☠
authorJim Chen <nchen@mozilla.com>
Thu, 21 Sep 2017 17:36:02 -0400
changeset 382289 5e5ce56330175c77e0470e2f5b7c2c6776be8f53
parent 382288 9f5bf3e0af8270ab31092655b4962848cf633df9
child 382290 71134284dc36a17d0a6293b596e5374d1482bcf9
push id51829
push usernchen@mozilla.com
push dateThu, 21 Sep 2017 21:39:18 +0000
treeherderautoland@0a367a71ca43 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1396951
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
mobile/android/app/src/main/res/values/arrays.xml
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
mobile/android/base/moz.build
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/HapticFeedbackDelegate.java
--- 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);
+}