diff options
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; + } +} |