Add utility methods to library for business logic

Additions to ApiLevelUtil were ultimately not relevant to GTS placement
test case migrated in this topic, but should be helpful for other use cases.

Also fix some exception handling for business logic actions.

bug: 62867056
Test: ./cts/run_unit_tests.sh
Change-Id: Ie2903b921be4ad2726ed3cfa6f2c1318effdba26
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/ApiLevelUtil.java b/common/device-side/util/src/com/android/compatibility/common/util/ApiLevelUtil.java
index 1df7198..c3394b7 100644
--- a/common/device-side/util/src/com/android/compatibility/common/util/ApiLevelUtil.java
+++ b/common/device-side/util/src/com/android/compatibility/common/util/ApiLevelUtil.java
@@ -18,6 +18,8 @@
 
 import android.os.Build;
 
+import java.lang.reflect.Field;
+
 /**
  * Device-side compatibility utility class for reading device API level.
  */
@@ -27,18 +29,34 @@
         return Build.VERSION.SDK_INT < version;
     }
 
+    public static boolean isBefore(String version) {
+        return Build.VERSION.SDK_INT < resolveVersionString(version);
+    }
+
     public static boolean isAfter(int version) {
         return Build.VERSION.SDK_INT > version;
     }
 
+    public static boolean isAfter(String version) {
+        return Build.VERSION.SDK_INT > resolveVersionString(version);
+    }
+
     public static boolean isAtLeast(int version) {
         return Build.VERSION.SDK_INT >= version;
     }
 
+    public static boolean isAtLeast(String version) {
+        return Build.VERSION.SDK_INT > resolveVersionString(version);
+    }
+
     public static boolean isAtMost(int version) {
         return Build.VERSION.SDK_INT <= version;
     }
 
+    public static boolean isAtMost(String version) {
+        return Build.VERSION.SDK_INT <= resolveVersionString(version);
+    }
+
     public static int getApiLevel() {
         return Build.VERSION.SDK_INT;
     }
@@ -54,4 +72,19 @@
     public static String getCodename() {
         return Build.VERSION.CODENAME;
     }
+
+    protected static int resolveVersionString(String versionString) {
+        try {
+            return Integer.parseInt(versionString); // e.g. "24" for M
+        } catch (NumberFormatException e1) {
+            try {
+                Field versionField = Build.VERSION_CODES.class.getField(
+                        versionString.toUpperCase());
+                return versionField.getInt(null); // no instance for VERSION_CODES, use null
+            } catch (IllegalAccessException | NoSuchFieldException e2) {
+                throw new RuntimeException(
+                        String.format("Failed to parse version string %s", versionString), e2);
+            }
+        }
+    }
 }
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
index 0694515..d8b0c6f 100644
--- a/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
+++ b/common/device-side/util/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
@@ -15,6 +15,9 @@
  */
 package com.android.compatibility.common.util;
 
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -67,6 +70,14 @@
         return InstrumentationRegistry.getInstrumentation().getTargetContext();
     }
 
+    public static void skipTest(String message) {
+        assumeTrue(message, false);
+    }
+
+    public static void failTest(String message) {
+        fail(message);
+    }
+
     public void mapPut(String mapName, String key, String value) {
         boolean put = false;
         for (Field f : getClass().getDeclaredFields()) {
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/FeatureUtil.java b/common/device-side/util/src/com/android/compatibility/common/util/FeatureUtil.java
new file mode 100644
index 0000000..5afea29
--- /dev/null
+++ b/common/device-side/util/src/com/android/compatibility/common/util/FeatureUtil.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.content.pm.PackageManager;
+import android.support.test.InstrumentationRegistry;
+
+/**
+ * Device-side utility class for detecting system features
+ */
+public class FeatureUtil {
+
+    public static boolean hasSystemFeature(String feature) {
+        return getPackageManager().hasSystemFeature(feature);
+    }
+
+    public static boolean lacksSystemFeature(String feature) {
+        return !hasSystemFeature(feature);
+    }
+
+    public static boolean hasAllSystemFeatures(String... features) {
+        PackageManager pm = getPackageManager();
+        for (String feature : features) {
+            if (!pm.hasSystemFeature(feature)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean lacksAllSystemFeatures(String... features) {
+        PackageManager pm = getPackageManager();
+        for (String feature : features) {
+            if (pm.hasSystemFeature(feature)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static PackageManager getPackageManager() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+    }
+}
diff --git a/common/device-side/util/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java b/common/device-side/util/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java
new file mode 100644
index 0000000..13305c3
--- /dev/null
+++ b/common/device-side/util/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.Build;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@line ApiLevelUtil}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ApiLevelUtilTest {
+
+    @Test
+    public void testComparisonByInt() throws Exception {
+        int version = Build.VERSION.SDK_INT;
+
+        assertFalse(ApiLevelUtil.isBefore(version - 1));
+        assertFalse(ApiLevelUtil.isBefore(version));
+        assertTrue(ApiLevelUtil.isBefore(version + 1));
+
+        assertTrue(ApiLevelUtil.isAfter(version - 1));
+        assertFalse(ApiLevelUtil.isAfter(version));
+        assertFalse(ApiLevelUtil.isAfter(version + 1));
+
+        assertTrue(ApiLevelUtil.isAtLeast(version - 1));
+        assertTrue(ApiLevelUtil.isAtLeast(version));
+        assertFalse(ApiLevelUtil.isAtLeast(version + 1));
+
+        assertFalse(ApiLevelUtil.isAtMost(version - 1));
+        assertTrue(ApiLevelUtil.isAtMost(version));
+        assertTrue(ApiLevelUtil.isAtMost(version + 1));
+    }
+
+    @Test
+    public void testComparisonByString() throws Exception {
+        // test should pass as long as device SDK version is at least 12
+        assertTrue(ApiLevelUtil.isAtLeast("HONEYCOMB_MR1"));
+        assertTrue(ApiLevelUtil.isAtLeast("12"));
+    }
+
+    @Test
+    public void testResolveVersionString() throws Exception {
+        // can only test versions known to the device build
+        assertEquals(ApiLevelUtil.resolveVersionString("GINGERBREAD_MR1"), 10);
+        assertEquals(ApiLevelUtil.resolveVersionString("10"), 10);
+        assertEquals(ApiLevelUtil.resolveVersionString("HONEYCOMB"), 11);
+        assertEquals(ApiLevelUtil.resolveVersionString("11"), 11);
+        assertEquals(ApiLevelUtil.resolveVersionString("honeycomb_mr1"), 12);
+        assertEquals(ApiLevelUtil.resolveVersionString("12"), 12);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testResolveMisspelledVersionString() throws Exception {
+        ApiLevelUtil.resolveVersionString("GINGERBEARD");
+    }
+}
diff --git a/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java b/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
index 578db9c..f7655b4 100644
--- a/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
+++ b/common/device-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
@@ -32,6 +32,8 @@
 import java.util.Arrays;
 import java.util.List;
 
+import junit.framework.AssertionFailedError;
+
 /**
  * Tests for {@line BusinessLogicDeviceExecutor}.
  */
@@ -48,6 +50,7 @@
     private static final String METHOD_6 = THIS_CLASS + ".method6";
     private static final String METHOD_7 = THIS_CLASS + ".method7";
     private static final String METHOD_8 = THIS_CLASS + ".method8";
+    private static final String METHOD_9 = THIS_CLASS + ".method9";
     private static final String FAKE_METHOD = THIS_CLASS + ".methodDoesntExist";
     private static final String ARG_STRING_1 = "arg1";
     private static final String ARG_STRING_2 = "arg2";
@@ -207,6 +210,11 @@
         assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
     }
 
+    @Test(expected = RuntimeException.class)
+    public void testExecuteActionThrowException() throws Exception {
+        mExecutor.executeAction(METHOD_9);
+    }
+
     public void method1() {
         mInvoked = METHOD_1;
     }
@@ -267,6 +275,11 @@
         // unsupported for the BusinessLogic service
     }
 
+    // throw AssertionFailedError
+    public void method9() throws AssertionFailedError {
+        assertTrue(false);
+    }
+
     public static class OtherClass {
 
         public static String otherInvoked = null;
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java
index 2940f86..c2fea21 100644
--- a/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/BusinessLogicHostExecutorTest.java
@@ -33,6 +33,8 @@
 import java.util.Arrays;
 import java.util.List;
 
+import junit.framework.AssertionFailedError;
+
 /**
  * Tests for {@link BusinessLogicHostExecutor}.
  */
@@ -49,6 +51,7 @@
     private static final String METHOD_6 = THIS_CLASS + ".method6";
     private static final String METHOD_7 = THIS_CLASS + ".method7";
     private static final String METHOD_8 = THIS_CLASS + ".method8";
+    private static final String METHOD_9 = THIS_CLASS + ".method9";
     private static final String FAKE_METHOD = THIS_CLASS + ".methodDoesntExist";
     private static final String ARG_STRING_1 = "arg1";
     private static final String ARG_STRING_2 = "arg2";
@@ -211,6 +214,11 @@
         assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
     }
 
+    @Test(expected = RuntimeException.class)
+    public void testExecuteActionThrowException() throws Exception {
+        mExecutor.executeAction(METHOD_9);
+    }
+
     public void method1() {
         mInvoked = METHOD_1;
     }
@@ -273,6 +281,11 @@
         // unsupported for the BusinessLogic service
     }
 
+    // throw AssertionFailedError
+    public void method9() throws AssertionFailedError {
+        assertTrue(false);
+    }
+
     public static class OtherClass {
 
         public static String otherInvoked = null;
diff --git a/common/util/src/com/android/compatibility/common/util/BusinessLogic.java b/common/util/src/com/android/compatibility/common/util/BusinessLogic.java
index 4b5c5af..3a5c523 100644
--- a/common/util/src/com/android/compatibility/common/util/BusinessLogic.java
+++ b/common/util/src/com/android/compatibility/common/util/BusinessLogic.java
@@ -140,7 +140,7 @@
         }
 
         /**
-         * Invoke this Business Logic condition with an executor.
+         * Invoke this Business Logic action with an executor.
          */
         public void invoke(BusinessLogicExecutor executor) {
             executor.executeAction(mMethodName,
diff --git a/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java b/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
index e189208..a74ad8a 100644
--- a/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
+++ b/common/util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
@@ -61,10 +61,17 @@
         try {
             invokeMethod(method, args);
         } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
-                InvocationTargetException | NoSuchMethodException e) {
+                NoSuchMethodException e) {
             throw new RuntimeException(String.format(
                     "BusinessLogic: Failed to invoke action method %s with args: %s", method,
                     Arrays.toString(args)), e);
+        } catch (InvocationTargetException e) {
+            // This action throws an exception, so throw the original exception (e.g.
+            // AssertionFailedError) for a more readable stacktrace.
+            Throwable t = e.getCause();
+            RuntimeException re = new RuntimeException(t.getMessage(), t.getCause());
+            re.setStackTrace(t.getStackTrace());
+            throw re;
         }
     }