summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sdl_android/build.gradle6
-rw-r--r--sdl_android/src/main/java/com/smartdevicelink/haptic/HapticInterfaceManager.java142
-rw-r--r--sdl_android/src/test/java/com/smartdevicelink/haptic/HapticInterfaceManagerTest.java183
3 files changed, 331 insertions, 0 deletions
diff --git a/sdl_android/build.gradle b/sdl_android/build.gradle
index a5615a867..ca93c3073 100644
--- a/sdl_android/build.gradle
+++ b/sdl_android/build.gradle
@@ -26,6 +26,10 @@ android {
lintOptions {
abortOnError false
}
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
}
dependencies {
@@ -37,6 +41,8 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'junit:junit:4.12'
+ testCompile 'org.mockito:mockito-core:2.9.0'
+
}
buildscript {
diff --git a/sdl_android/src/main/java/com/smartdevicelink/haptic/HapticInterfaceManager.java b/sdl_android/src/main/java/com/smartdevicelink/haptic/HapticInterfaceManager.java
new file mode 100644
index 000000000..e02177db1
--- /dev/null
+++ b/sdl_android/src/main/java/com/smartdevicelink/haptic/HapticInterfaceManager.java
@@ -0,0 +1,142 @@
+/***************************************************************************************************
+ * Copyright © 2017 Xevo Inc.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior written
+ * permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **************************************************************************************************/
+package com.smartdevicelink.haptic;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.smartdevicelink.exception.SdlException;
+import com.smartdevicelink.proxy.SdlProxyBase;
+import com.smartdevicelink.proxy.rpc.HapticRect;
+import com.smartdevicelink.proxy.rpc.Rectangle;
+import com.smartdevicelink.proxy.rpc.SendHapticData;
+import com.smartdevicelink.util.CorrelationIdGenerator;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created on 9/22/2017.
+ *
+ * Manages haptic data used to render focusable areas on the HU screen. App developers can
+ * over-ride the default logic used to find focusable Views by passing their own data to
+ * {@link #setHapticData(List)}
+ */
+public class HapticInterfaceManager {
+ private static final String TAG = "Haptic";
+
+ private WeakReference<SdlProxyBase> proxyHolder;
+ private List<HapticRect> userHapticData;
+
+ /**
+ * Sets haptic data and sends update to the HU. To be used by app code instead of letting
+ * Presentation find the Views and automatically send to HU.
+ *
+ * @param hapticData
+ * Rect data indicating "focusable" screen elements or areas
+ */
+ public void setHapticData(List<HapticRect> hapticData) {
+ userHapticData = hapticData;
+ SdlProxyBase proxy = proxyHolder.get();
+ if (proxy != null) {
+ SendHapticData msg = new SendHapticData();
+ msg.setHapticRectData(userHapticData);
+ try {
+ proxy.sendRPCRequest(msg);
+ } catch (SdlException e) {
+ Log.e(TAG, "failed to send user haptic RPC", e);
+ }
+ }
+ }
+
+ public HapticInterfaceManager(SdlProxyBase proxy) {
+ this.proxyHolder = new WeakReference<>(proxy);
+ }
+
+ /**
+ * Sends haptic data found by searching for focusable and clickable Views in the view heirarchy
+ * to the HU. Should be called by Presentation's OnShowListener.
+ *
+ * @param root
+ * the root or parent View
+ */
+ public void refreshHapticData(View root) {
+ SdlProxyBase proxy = proxyHolder.get();
+ if ((userHapticData == null) && (proxy != null)) {
+ List<HapticRect> hapticRects = new ArrayList<>();
+ findHapticRects(root, hapticRects);
+
+ SendHapticData msg = new SendHapticData();
+ msg.setHapticRectData(hapticRects);
+
+ try {
+ proxy.sendRPCRequest(msg);
+ } catch (SdlException e) {
+ Log.e(TAG, "failed to send haptic RPC", e);
+ }
+ }
+ }
+
+ private void findHapticRects(View root, final List<HapticRect> hapticRects) {
+ List<View> focusables = new ArrayList<>();
+ getFocusableViews(root, focusables);
+
+ int [] loc = new int[2];
+ int id = 0;
+ for (View view : focusables) {
+ int w = view.getWidth();
+ int h = view.getHeight();
+ view.getLocationOnScreen(loc);
+
+ Rectangle rect = new Rectangle();
+ rect.setWidth((float) w);
+ rect.setHeight((float) h);
+ rect.setX((float) loc[0]);
+ rect.setY((float) loc[1]);
+
+ HapticRect hapticRect = new HapticRect();
+ hapticRect.setId(id++);
+ hapticRect.setRect(rect);
+ hapticRects.add(hapticRect);
+ }
+ }
+
+ private void getFocusableViews(View view, final List<View> focusables) {
+ // Not using addFocusables() or addTouchables() because of concerns with adding ViewGroup
+ // and not getting "clickables."
+
+ if (!(view instanceof ViewGroup) && (view != null) &&
+ (view.isFocusable() || view.isClickable())) {
+ focusables.add(view);
+ }
+
+ if (view instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) view;
+
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ getFocusableViews(parent.getChildAt(i), focusables);
+ }
+ }
+ }
+}
diff --git a/sdl_android/src/test/java/com/smartdevicelink/haptic/HapticInterfaceManagerTest.java b/sdl_android/src/test/java/com/smartdevicelink/haptic/HapticInterfaceManagerTest.java
new file mode 100644
index 000000000..438042cc1
--- /dev/null
+++ b/sdl_android/src/test/java/com/smartdevicelink/haptic/HapticInterfaceManagerTest.java
@@ -0,0 +1,183 @@
+/***************************************************************************************************
+ * Copyright © 2017 Xevo Inc.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior written
+ * permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **************************************************************************************************/
+package com.smartdevicelink.haptic;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.smartdevicelink.exception.SdlException;
+import com.smartdevicelink.proxy.SdlProxyBase;
+import com.smartdevicelink.proxy.rpc.HapticRect;
+import com.smartdevicelink.proxy.rpc.Rectangle;
+import com.smartdevicelink.proxy.rpc.SendHapticData;
+
+import junit.framework.TestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * Created on 9/26/2017.
+ */
+@RunWith(MockitoJUnitRunner.Strict.class)
+public class HapticInterfaceManagerTest extends TestCase {
+ @Mock
+ private SdlProxyBase mockProxy;
+
+ @Captor
+ private ArgumentCaptor<SendHapticData> captor;
+
+ private HapticInterfaceManager hapticMgr;
+
+ @Before
+ public void setUp() throws Exception {
+ hapticMgr = new HapticInterfaceManager(mockProxy);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ hapticMgr = null;
+ }
+
+ @Test
+ public void testSetHapticData() throws Exception {
+ List<HapticRect> rects = new ArrayList<>();
+ Rectangle rect = new Rectangle();
+ rect.setX(10f);
+ rect.setY(10f);
+ rect.setWidth(50f);
+ rect.setHeight(20f);
+ HapticRect hRect = new HapticRect();
+ hRect.setRect(rect);
+ rects.add(hRect);
+ hapticMgr.setHapticData(rects);
+ verify(mockProxy).sendRPCRequest(any(SendHapticData.class));
+ }
+
+ @Test
+ public void testSetHapticDataException() throws Exception {
+ doThrow(SdlException.class).when(mockProxy).sendRPCRequest(any(SendHapticData.class));
+ hapticMgr.setHapticData(null);
+ }
+
+ @Test
+ public void testRefreshHapticData() throws Exception {
+ View root = createViews();
+ hapticMgr.refreshHapticData(root);
+ verify(mockProxy).sendRPCRequest(captor.capture());
+ SendHapticData data = captor.getValue();
+ assertNotNull("SendHapticData RPC", data);
+ List<HapticRect> list = data.getHapticRectData();
+ assertNotNull("List", list);
+ assertEquals("Haptic Rects", 4, list.size());
+ }
+
+ @Test
+ public void testRefreshHapticDataException() throws Exception {
+ doThrow(SdlException.class).when(mockProxy).sendRPCRequest(any(SendHapticData.class));
+ View root = createViews();
+ hapticMgr.refreshHapticData(root);
+ }
+
+ @Test
+ public void testRefreshHapticDataNull() throws Exception {
+ hapticMgr.refreshHapticData(null);
+ verify(mockProxy).sendRPCRequest(captor.capture());
+ SendHapticData data = captor.getValue();
+ assertNotNull("SendHapticData RPC", data);
+ List<HapticRect> list = data.getHapticRectData();
+ assertNull("List", list);
+ }
+
+ @Test
+ public void testRefreshWithUserData() throws Exception {
+ List<HapticRect> rects = new ArrayList<>();
+ Rectangle rect = new Rectangle();
+ rect.setX(10f);
+ rect.setY(10f);
+ rect.setWidth(50f);
+ rect.setHeight(20f);
+ HapticRect hRect = new HapticRect();
+ hRect.setRect(rect);
+ rects.add(hRect);
+ hapticMgr.setHapticData(rects);
+ verify(mockProxy).sendRPCRequest(any(SendHapticData.class));
+
+ View root = createViews();
+ hapticMgr.refreshHapticData(root);
+ verify(mockProxy, times(1)).sendRPCRequest(any(SendHapticData.class));
+ }
+
+ private View createViews() {
+
+ View view = mock(View.class);
+
+ ViewGroup parent1 = mock(ViewGroup.class);
+ ViewGroup parent2 = mock(ViewGroup.class);
+
+ when(parent1.getChildCount()).thenReturn(5);
+
+ when(parent1.getChildAt(0)).thenReturn(view);
+ when(parent1.getChildAt(1)).thenReturn(view);
+ when(parent1.getChildAt(2)).thenReturn(view);
+ when(parent1.getChildAt(3)).thenReturn(parent2);
+ when(parent1.getChildAt(4)).thenReturn(view);
+
+ when(parent2.getChildCount()).thenReturn(2);
+ when(parent2.getChildAt(0)).thenReturn(view);
+ when(parent2.getChildAt(1)).thenReturn(view);
+
+ when(view.isFocusable()).then(new Answer<Boolean>() {
+ private int count = 0;
+
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ int curCount = count++;
+ return (curCount == 1) || (curCount == 2) || (curCount == 3);
+ }
+ });
+ when(view.isClickable()).then(new Answer<Boolean>() {
+ private int count = 0;
+
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ int curCount = count++;
+ return (curCount == 0) || (curCount == 3);
+ }
+ });
+
+ return parent1;
+ }
+}