Bug 1396951 - 1. Add and use HapticFeedbackDelegate; r?snorp draft
authorJim Chen <nchen@mozilla.com>
Fri, 22 Sep 2017 14:35:22 -0400
changeset 669222 017ef992e6f0a04fb7792619754802614c675eba
parent 669023 2cd3752963fc8f24f7c202687eab55e83222f608
child 669223 751c731fbc6ee85b972e29a8a02abfcacaf16f6a
push id81260
push userbmo:nchen@mozilla.com
push dateFri, 22 Sep 2017 18:35:47 +0000
reviewerssnorp
bugs1396951
milestone58.0a1
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);
+}