Added new package for Wearable Input helpers.
This CL adds in WearableButtons from the Wearable Support Library to a
new AndroidX artifact, androidx.wear:wear-input. This also adds in
androidx.wear:wear-input-testing, which includes the test
implementations of WearableButtonsProvider to ease testing.
Bug: 156717151
Test: Tests migrated from WSL and updated to not require Robolectric
shadows
Change-Id: I0ed0ce2a891e982afea3517e3309153751ae4ac4
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 8c3a5ec..24180c7 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -102,7 +102,7 @@
val VERSIONEDPARCELABLE = LibraryGroup("androidx.versionedparcelable", null)
val VIEWPAGER = LibraryGroup("androidx.viewpager", LibraryVersions.VIEWPAGER)
val VIEWPAGER2 = LibraryGroup("androidx.viewpager2", LibraryVersions.VIEWPAGER2)
- val WEAR = LibraryGroup("androidx.wear", LibraryVersions.WEAR)
+ val WEAR = LibraryGroup("androidx.wear", null)
val WEBKIT = LibraryGroup("androidx.webkit", LibraryVersions.WEBKIT)
val WINDOW = LibraryGroup("androidx.window", null)
val WORK = LibraryGroup("androidx.work", LibraryVersions.WORK)
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 0918394..116e2c0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -117,6 +117,7 @@
val VIEWPAGER = Version("1.1.0-alpha01")
val VIEWPAGER2 = Version("1.1.0-alpha02")
val WEAR = Version("1.2.0-alpha01")
+ val WEAR_INPUT = Version("1.0.0-alpha01")
val WEBKIT = Version("1.3.0-rc01")
val WINDOW = Version("1.0.0-alpha02")
val WINDOW_SIDECAR = Version("0.1.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index 7659c54..fd7c65f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -143,8 +143,10 @@
prebuilts(LibraryGroups.VERSIONEDPARCELABLE, "versionedparcelable", "1.1.1")
prebuilts(LibraryGroups.VIEWPAGER, "1.0.0")
prebuilts(LibraryGroups.VIEWPAGER2, "1.1.0-alpha01")
- prebuilts(LibraryGroups.WEAR, "1.1.0-rc01")
+ prebuilts(LibraryGroups.WEAR, "wear", "1.1.0-rc01")
.addStubs("wear/wear_stubs/com.google.android.wearable-stubs.jar")
+ ignore(LibraryGroups.WEAR.group, "wear-input")
+ ignore(LibraryGroups.WEAR.group, "wear-input-testing")
prebuilts(LibraryGroups.WEBKIT, "1.3.0-alpha03")
ignore(LibraryGroups.WINDOW.group, "window-sidecar")
prebuilts(LibraryGroups.WINDOW, "1.0.0-alpha01")
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index 75b29a9..f3371e7 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -493,6 +493,14 @@
"to": "ignore"
},
{
+ "from": "androidx/wear/input/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/wear/input/testing/(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/benchmark/(.*)",
"to": "ignore"
},
@@ -1107,6 +1115,14 @@
"to": "androidx/wear"
},
{
+ "from": "androidx/wear/input",
+ "to": "androidx/wear/input"
+ },
+ {
+ "from": "androidx/wear/input/testing",
+ "to": "androidx/wear/input/testing"
+ },
+ {
"from": "androidx/emoji/appcompat",
"to": "androidx/emoji/appcompat"
},
@@ -2066,6 +2082,30 @@
},
{
"from": {
+ "groupId": "androidx.wear",
+ "artifactId": "wear-input",
+ "version": "{newSlVersion}"
+ },
+ "to": {
+ "groupId": "androidx.wear",
+ "artifactId": "wear-input",
+ "version": "{newSlVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "androidx.wear",
+ "artifactId": "wear-input-testing",
+ "version": "{newSlVersion}"
+ },
+ "to": {
+ "groupId": "androidx.wear",
+ "artifactId": "wear-input-testing",
+ "version": "{newSlVersion}"
+ }
+ },
+ {
+ "from": {
"groupId": "androidx.asynclayoutinflater",
"artifactId": "asynclayoutinflater",
"version": "{newSlVersion}"
diff --git a/settings.gradle b/settings.gradle
index 3743d2b..0baccbf 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -320,6 +320,8 @@
includeProject(":viewpager2:viewpager2", "viewpager2/viewpager2")
includeProject(":viewpager2:integration-tests:testapp", "viewpager2/integration-tests/testapp")
includeProject(":wear:wear", "wear/wear")
+includeProject(":wear:wear-input", "wear/wear-input")
+includeProject(":wear:wear-input-testing", "wear/wear-input-testing")
includeProject(":webkit:webkit", "webkit/webkit")
includeProject(":webkit:integration-tests:testapp", "webkit/integration-tests/testapp")
includeProject(":window:window", "window/window")
diff --git a/wear/wear-input-testing/api/1.0.0-alpha01.txt b/wear/wear-input-testing/api/1.0.0-alpha01.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/1.0.0-alpha01.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/current.txt b/wear/wear-input-testing/api/current.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/current.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/public_plus_experimental_1.0.0-alpha01.txt b/wear/wear-input-testing/api/public_plus_experimental_1.0.0-alpha01.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/public_plus_experimental_1.0.0-alpha01.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/public_plus_experimental_current.txt b/wear/wear-input-testing/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/public_plus_experimental_current.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/res-1.0.0-alpha01.txt b/wear/wear-input-testing/api/res-1.0.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input-testing/api/res-1.0.0-alpha01.txt
diff --git a/wear/wear-input-testing/api/res-current.txt b/wear/wear-input-testing/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input-testing/api/res-current.txt
diff --git a/wear/wear-input-testing/api/restricted_1.0.0-alpha01.txt b/wear/wear-input-testing/api/restricted_1.0.0-alpha01.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/restricted_1.0.0-alpha01.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/restricted_current.txt b/wear/wear-input-testing/api/restricted_current.txt
new file mode 100644
index 0000000..847f64c
--- /dev/null
+++ b/wear/wear-input-testing/api/restricted_current.txt
@@ -0,0 +1,18 @@
+// Signature format: 3.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/build.gradle b/wear/wear-input-testing/build.gradle
new file mode 100644
index 0000000..9e70953
--- /dev/null
+++ b/wear/wear-input-testing/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ api(project(":wear:wear-input"))
+ compileOnly fileTree(dir: '../wear_stubs', include: ['com.google.android.wearable-stubs.jar'])
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+}
+
+androidx {
+ name = "Android Wear Support Input Testing Helpers"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.WEAR
+ mavenVersion = LibraryVersions.WEAR_INPUT
+ inceptionYear = "2020"
+ description = "Android Wear Support Input Testing Helpers"
+}
diff --git a/wear/wear-input-testing/src/main/AndroidManifest.xml b/wear/wear-input-testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..70ec2c9
--- /dev/null
+++ b/wear/wear-input-testing/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 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.
+ -->
+<manifest package="androidx.wear"/>
diff --git a/wear/wear-input-testing/src/main/java/androidx/wear/input/testing/TestWearableButtonsProvider.java b/wear/wear-input-testing/src/main/java/androidx/wear/input/testing/TestWearableButtonsProvider.java
new file mode 100644
index 0000000..b2ddfc5
--- /dev/null
+++ b/wear/wear-input-testing/src/main/java/androidx/wear/input/testing/TestWearableButtonsProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2020 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 androidx.wear.input.testing;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.wear.input.WearableButtonsProvider;
+
+import com.google.android.wearable.input.WearableInputDevice;
+
+import java.util.Map;
+
+/**
+ * A {@link WearableButtonsProvider} suitable for use in tests.
+ *
+ * <p>This allows for explicitly specifying which buttons are available for testing, and their
+ * coordinates. It is intended to be used by passing in a map, mapping between the button keycode
+ * (typically in the set {@link android.view.KeyEvent#KEYCODE_STEM_PRIMARY}, {@link
+ * android.view.KeyEvent#KEYCODE_STEM_1}, {@link android.view.KeyEvent#KEYCODE_STEM_2}, or {@link
+ * android.view.KeyEvent#KEYCODE_STEM_3}) and the location of the button. Take the following
+ * example:
+ *
+ * <pre>
+ * Map<Integer, TestWearableButtonLocation> buttons = new HashMap<>();
+ * buttons.put(KEYCODE_STEM_1, new TestWearableButtonLocation(100, 100);
+ *
+ * TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ *
+ * WearableButtons.setWearableButtonsProvider(provider);
+ * </pre>
+ */
+public class TestWearableButtonsProvider implements WearableButtonsProvider {
+
+ /**
+ * Class describing the location of a button on a wearable device. This has two forms; it can
+ * either store the absolute location of the button, or store both the absolute location of the
+ * button, and the absolute location when the screen is rotated through 180 degrees.
+ */
+ public static class TestWearableButtonLocation {
+ private final PointF mLocation;
+ private final PointF mRotatedLocation;
+
+ /**
+ * Build a button location, with just the default button location.
+ *
+ * @param x X coordinate of the button.
+ * @param y Y coordinate of the button.
+ */
+ public TestWearableButtonLocation(float x, float y) {
+ mLocation = new PointF(x, y);
+ mRotatedLocation = null;
+ }
+
+ /**
+ * Build a button location, with both the default button location, and the location when the
+ * device is rotated through 180 degrees.
+ *
+ * @param x X coordinate of the button.
+ * @param y Y coordinate of the button.
+ * @param rotatedX X coordinate of the button when the device is rotated.
+ * @param rotatedY Y coordinate of the button when the device is rotated.
+ */
+ public TestWearableButtonLocation(float x, float y, float rotatedX, float rotatedY) {
+ mLocation = new PointF(x, y);
+ mRotatedLocation = new PointF(rotatedX, rotatedY);
+ }
+
+ /**
+ * Get the location of this button.
+ *
+ * @return A point specifying the location of this button.
+ */
+ @NonNull
+ public PointF getLocation() {
+ return mLocation;
+ }
+
+ /**
+ * Get the location of this button when the device is rotated.
+ *
+ * @return A point specifying the location of this button when the device is rotated.
+ */
+ @Nullable
+ public PointF getRotatedLocation() {
+ return mRotatedLocation;
+ }
+ }
+
+ private final Map<Integer, TestWearableButtonLocation> mButtons;
+
+ /**
+ * Build a button provider, which will respond with the provided set of buttons.
+ *
+ * @param buttons The buttons returned by this provider.
+ */
+ public TestWearableButtonsProvider(@NonNull Map<Integer, TestWearableButtonLocation> buttons) {
+ mButtons = buttons;
+ }
+
+ @NonNull
+ @Override
+ public Bundle getButtonInfo(@NonNull Context context, int keycode) {
+ Bundle bundle = new Bundle();
+
+ TestWearableButtonLocation location = mButtons.get(keycode);
+ if (location != null) {
+ bundle.putFloat(WearableInputDevice.X_KEY, location.getLocation().x);
+ bundle.putFloat(WearableInputDevice.Y_KEY, location.getLocation().y);
+
+ if (location.getRotatedLocation() != null) {
+ bundle.putFloat(WearableInputDevice.X_KEY_ROTATED, location.getRotatedLocation().x);
+ bundle.putFloat(WearableInputDevice.Y_KEY_ROTATED, location.getRotatedLocation().y);
+ }
+ }
+
+ return bundle;
+ }
+
+ @Nullable
+ @Override
+ public int[] getAvailableButtonKeyCodes(@NonNull Context context) {
+ int[] keys = new int[mButtons.size()];
+
+ int i = 0;
+ for (Integer keycode : mButtons.keySet()) {
+ keys[i++] = keycode;
+ }
+
+ return keys;
+ }
+}
diff --git a/wear/wear-input/api/1.0.0-alpha01.txt b/wear/wear-input/api/1.0.0-alpha01.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/1.0.0-alpha01.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/current.txt b/wear/wear-input/api/current.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/current.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/public_plus_experimental_1.0.0-alpha01.txt b/wear/wear-input/api/public_plus_experimental_1.0.0-alpha01.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/public_plus_experimental_1.0.0-alpha01.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/public_plus_experimental_current.txt b/wear/wear-input/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/public_plus_experimental_current.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/res-1.0.0-alpha01.txt b/wear/wear-input/api/res-1.0.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input/api/res-1.0.0-alpha01.txt
diff --git a/wear/wear-input/api/res-current.txt b/wear/wear-input/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input/api/res-current.txt
diff --git a/wear/wear-input/api/restricted_1.0.0-alpha01.txt b/wear/wear-input/api/restricted_1.0.0-alpha01.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/restricted_1.0.0-alpha01.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/restricted_current.txt b/wear/wear-input/api/restricted_current.txt
new file mode 100644
index 0000000..9998442
--- /dev/null
+++ b/wear/wear-input/api/restricted_current.txt
@@ -0,0 +1,30 @@
+// Signature format: 3.0
+package androidx.wear.input {
+
+ public class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/build.gradle b/wear/wear-input/build.gradle
new file mode 100644
index 0000000..5ab8fb0
--- /dev/null
+++ b/wear/wear-input/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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 Laicense.
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+
+ testImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ testImplementation(ANDROIDX_TEST_CORE)
+ testImplementation(ANDROIDX_TEST_RUNNER)
+ testImplementation(ANDROIDX_TEST_RULES)
+ testImplementation(ROBOLECTRIC)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(project(":wear:wear-input-testing"))
+
+ compileOnly fileTree(dir: '../wear_stubs', include: ['com.google.android.wearable-stubs.jar'])
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+ name = "Android Wear Support Input"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.WEAR
+ mavenVersion = LibraryVersions.WEAR_INPUT
+ inceptionYear = "2020"
+ description = "Android Wear Support Input"
+}
diff --git a/wear/wear-input/src/main/AndroidManifest.xml b/wear/wear-input/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..19e4723a
--- /dev/null
+++ b/wear/wear-input/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 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.
+ -->
+<manifest package="androidx.wear.input"/>
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/DeviceWearableButtonsProvider.java b/wear/wear-input/src/main/java/androidx/wear/input/DeviceWearableButtonsProvider.java
new file mode 100644
index 0000000..a8130fb
--- /dev/null
+++ b/wear/wear-input/src/main/java/androidx/wear/input/DeviceWearableButtonsProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 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 androidx.wear.input;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.wearable.input.WearableInputDevice;
+
+/**
+ * Default implementation of {@link WearableButtonsProvider}, that reads the button locations from
+ * the platform.
+ */
+public class DeviceWearableButtonsProvider implements WearableButtonsProvider {
+
+ @NonNull
+ @Override
+ public Bundle getButtonInfo(@NonNull Context context, int keycode) {
+ if (!isSharedLibAvailable()) {
+ return null;
+ }
+
+ return WearableInputDevice.getButtonInfo(context, keycode);
+ }
+
+ @Nullable
+ @Override
+ public int[] getAvailableButtonKeyCodes(@NonNull Context context) {
+ if (!isSharedLibAvailable()) {
+ return null;
+ }
+
+ return WearableInputDevice.getAvailableButtonKeyCodes(context);
+ }
+
+ private boolean isSharedLibAvailable() {
+ return SharedLibraryVersion.version() >= 1;
+ }
+}
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/SharedLibraryVersion.java b/wear/wear-input/src/main/java/androidx/wear/input/SharedLibraryVersion.java
new file mode 100644
index 0000000..2a6b0c9
--- /dev/null
+++ b/wear/wear-input/src/main/java/androidx/wear/input/SharedLibraryVersion.java
@@ -0,0 +1,102 @@
+/*
+ * 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 androidx.wear.input;
+
+import android.os.Build;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.android.wearable.WearableSharedLib;
+
+/**
+ * Internal class which can be used to determine the version of the wearable shared library that is
+ * available on the current device.
+ */
+final class SharedLibraryVersion {
+
+ private SharedLibraryVersion() {}
+
+ /**
+ * Returns the version of the wearable shared library available on the current device.
+ *
+ * <p>
+ *
+ * <p>Version 1 was introduced on 2016-09-26, so any previous shared library will return 0. In
+ * those cases, it may be necessary to check {@code Build.VERSION.SDK_INT}.
+ *
+ * @throws IllegalStateException if the Wearable Shared Library is not present, which means that
+ * the {@code <uses-library>} tag is missing.
+ */
+ public static int version() {
+ verifySharedLibraryPresent();
+ return VersionHolder.VERSION;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if the Wearable Shared Library is not present and API
+ * level is at least LMP MR1.
+ *
+ * <p>
+ *
+ * <p>This validates that the developer hasn't forgotten to include a {@code <uses-library>} tag
+ * in their manifest. The method should be used in combination with API level checks for
+ * features added before {@link #version() version} 1.
+ */
+ public static void verifySharedLibraryPresent() {
+ if (!PresenceHolder.PRESENT) {
+ throw new IllegalStateException(
+ "Could not find wearable shared library classes. Please add <uses-library"
+ + " android:name=\"com.google.android.wearable\""
+ + " android:required=\"false\" /> to the application manifest");
+ }
+ }
+
+ // Lazy initialization holder class (see Effective Java item 71)
+ @VisibleForTesting
+ static final class VersionHolder {
+ static final int VERSION = getSharedLibVersion(Build.VERSION.SDK_INT);
+
+ @VisibleForTesting
+ static int getSharedLibVersion(int sdkInt) {
+ if (sdkInt < Build.VERSION_CODES.N_MR1) {
+ // WearableSharedLib was introduced in N MR1 (Wear FDP 4)
+ return 0;
+ }
+ return WearableSharedLib.version();
+ }
+
+ private VersionHolder() {}
+ }
+
+ // Lazy initialization holder class (see Effective Java item 71)
+ @VisibleForTesting
+ static final class PresenceHolder {
+ static final boolean PRESENT = isSharedLibPresent(Build.VERSION.SDK_INT);
+
+ @VisibleForTesting
+ static boolean isSharedLibPresent(int sdkInt) {
+ try {
+ // A class which has been available on the shared library from the first version.
+ Class.forName("com.google.android.wearable.compat.WearableActivityController");
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+
+ private PresenceHolder() {}
+ }
+}
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java
new file mode 100644
index 0000000..a2ec7da
--- /dev/null
+++ b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java
@@ -0,0 +1,722 @@
+/*
+ * Copyright 2020 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 androidx.wear.input;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RotateDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.android.wearable.input.WearableInputDevice;
+
+/** Class containing helpers for managing wearable buttons. */
+@TargetApi(Build.VERSION_CODES.M)
+public final class WearableButtons {
+
+ private static WearableButtonsProvider sButtonsProvider = new DeviceWearableButtonsProvider();
+
+ /** Represents that the count of available device buttons. */
+ private static volatile int sButtonCount = -1;
+
+ private WearableButtons() {
+ throw new RuntimeException("WearableButtons should not be instantiated");
+ }
+
+ /**
+ * Testing call to allow the underlying {@link WearableButtonsProvider} to be substituted in
+ * test code.
+ *
+ * @param provider The new {@link WearableButtonsProvider} to use.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setWearableButtonsProvider(@NonNull WearableButtonsProvider provider) {
+ sButtonsProvider = provider;
+ }
+
+ /** Represents that the location zone is unknown. */
+ @VisibleForTesting static final int LOC_UNKNOWN = -1;
+
+ /** Represents the east position on a round device. */
+ @VisibleForTesting static final int LOC_EAST = 0;
+
+ /** Represents the east-northeast position on a round device. */
+ @VisibleForTesting static final int LOC_ENE = 1;
+
+ /** Represents the northeast position on a round device. */
+ @VisibleForTesting static final int LOC_NE = 2;
+
+ /** Represents the north-northeast position on a round device. */
+ @VisibleForTesting static final int LOC_NNE = 3;
+
+ /** Represents the north position on a round device. */
+ @VisibleForTesting static final int LOC_NORTH = 4;
+
+ /** Represents the north-northwest position on a round device. */
+ @VisibleForTesting static final int LOC_NNW = 5;
+
+ /** Represents the northwest position on a round device. */
+ @VisibleForTesting static final int LOC_NW = 6;
+
+ /** Represents the west-northwest position on a round device. */
+ @VisibleForTesting static final int LOC_WNW = 7;
+
+ /** Represents the west position on a round device. */
+ @VisibleForTesting static final int LOC_WEST = 8;
+
+ /** Represents the west-southwest position on a round device. */
+ @VisibleForTesting static final int LOC_WSW = 9;
+
+ /** Represents the southwest position on a round device. */
+ @VisibleForTesting static final int LOC_SW = 10;
+
+ /** Represents the south-southwest position on a round device. */
+ @VisibleForTesting static final int LOC_SSW = 11;
+
+ /** Represents the south position on a round device. */
+ @VisibleForTesting static final int LOC_SOUTH = 12;
+
+ /** Represents the south-southeast position on a round device. */
+ @VisibleForTesting static final int LOC_SSE = 13;
+
+ /** Represents the southeast position on a round device. */
+ @VisibleForTesting static final int LOC_SE = 14;
+
+ /** Represents the east-southeast position on a round device. */
+ @VisibleForTesting static final int LOC_ESE = 15;
+
+ private static final int LOC_ROUND_COUNT = 16;
+
+ /** Represents the right third of the top side on a square device. */
+ @VisibleForTesting static final int LOC_TOP_RIGHT = 100;
+
+ /** Represents the center third of the top side on a square device. */
+ @VisibleForTesting static final int LOC_TOP_CENTER = 101;
+
+ /** Represents the left third of the top side on a square device. */
+ @VisibleForTesting static final int LOC_TOP_LEFT = 102;
+
+ /** Represents the top third of the left side on a square device. */
+ @VisibleForTesting static final int LOC_LEFT_TOP = 103;
+
+ /** Represents the center third of the left side on a square device. */
+ @VisibleForTesting static final int LOC_LEFT_CENTER = 104;
+
+ /** Represents the bottom third of the left side on a square device. */
+ @VisibleForTesting static final int LOC_LEFT_BOTTOM = 105;
+
+ /** Represents the left third of the bottom side on a square device. */
+ @VisibleForTesting static final int LOC_BOTTOM_LEFT = 106;
+
+ /** Represents the center third of the bottom side on a square device. */
+ @VisibleForTesting static final int LOC_BOTTOM_CENTER = 107;
+
+ /** Represents the right third of the bottom side on a square device. */
+ @VisibleForTesting static final int LOC_BOTTOM_RIGHT = 108;
+
+ /** Represents the bottom third of the right side on a square device. */
+ @VisibleForTesting static final int LOC_RIGHT_BOTTOM = 109;
+
+ /** Represents the center third of the right side on a square device. */
+ @VisibleForTesting static final int LOC_RIGHT_CENTER = 110;
+
+ /** Represents the top third of the right side on a square device. */
+ @VisibleForTesting static final int LOC_RIGHT_TOP = 111;
+
+ /**
+ * Key used with the bundle returned by {@link #getButtonInfo}} to retrieve the x coordinate of
+ * a button when the screen is rotated 180 degrees. (temporary copy from WearableInputDevice)
+ */
+ private static final String X_KEY_ROTATED = "x_key_rotated";
+
+ /**
+ * Key used with the bundle returned by {@link #getButtonInfo}} to retrieve the y coordinate of
+ * a button when the screen is rotated 180 degrees. (temporary copy from WearableInputDevice)
+ */
+ private static final String Y_KEY_ROTATED = "y_key_rotated";
+
+ /**
+ * Returns a {@link ButtonInfo} containing the metadata for a specific button.
+ *
+ * <p>The location will be populated in the following manner:
+ *
+ * <ul>
+ * <li>The provided point will be on the screen, or more typically, on the edge of the screen.
+ * <li>The point won't be off the edge of the screen.
+ * <li>The location returned is a screen coordinate. The unit of measurement is in pixels. The
+ * coordinates do not take rotation into account and assume that the device is in the
+ * standard upright position.
+ * </ul>
+ *
+ * <p>Common keycodes to use are {@link android.view.KeyEvent#KEYCODE_STEM_PRIMARY}, {@link
+ * android.view.KeyEvent#KEYCODE_STEM_1}, {@link android.view.KeyEvent#KEYCODE_STEM_2}, and
+ * {@link android.view.KeyEvent#KEYCODE_STEM_3}.
+ *
+ * @param context The context of the current activity
+ * @param keycode The keycode associated with the hardware button of interest
+ * @return A {@link ButtonInfo} containing the metadata for the given keycode or null if the
+ * information is not available
+ */
+ @Nullable
+ public static ButtonInfo getButtonInfo(@NonNull Context context, int keycode) {
+ Bundle bundle = sButtonsProvider.getButtonInfo(context, keycode);
+
+ if (bundle == null) {
+ return null;
+ }
+
+ // If the information is not available, return null
+ if (!bundle.containsKey(WearableInputDevice.X_KEY)
+ || !bundle.containsKey(WearableInputDevice.Y_KEY)) {
+ return null;
+ }
+
+ float screenLocationX = bundle.getFloat(WearableInputDevice.X_KEY);
+ float screenLocationY = bundle.getFloat(WearableInputDevice.Y_KEY);
+
+ // Get the screen size for the locationZone
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Point screenSize = new Point();
+ wm.getDefaultDisplay().getSize(screenSize);
+
+ if (isLeftyModeEnabled(context)) {
+ // By default, the rotated placement is exactly the opposite.
+ // This may be overridden if there is a remapping of buttons applied as well.
+ float screenRotatedX = screenSize.x - screenLocationX;
+ float screenRotatedY = screenSize.y - screenLocationY;
+
+ if (bundle.containsKey(X_KEY_ROTATED) && bundle.containsKey(Y_KEY_ROTATED)) {
+ screenRotatedX = bundle.getFloat(X_KEY_ROTATED);
+ screenRotatedY = bundle.getFloat(Y_KEY_ROTATED);
+ }
+
+ screenLocationX = screenRotatedX;
+ screenLocationY = screenRotatedY;
+ }
+
+ boolean isRound = context.getResources().getConfiguration().isScreenRound();
+
+ ButtonInfo info =
+ new ButtonInfo(
+ keycode,
+ screenLocationX,
+ screenLocationY,
+ getLocationZone(isRound, screenSize, screenLocationX, screenLocationY));
+
+ return info;
+ }
+
+ /**
+ * Get the number of hardware buttons available. This function only works on API level 24 or
+ * higher (Wear 2.0). This count includes the primary stem key as well as any secondary stem
+ * keys available.
+ *
+ * @param context The context of the current activity
+ * @return The number of buttons available or -1 if not a Wear 2.0 device.
+ */
+ public static int getButtonCount(@NonNull Context context) {
+ if (sButtonCount == -1) {
+ int[] buttonCodes = sButtonsProvider.getAvailableButtonKeyCodes(context);
+
+ if (buttonCodes == null) {
+ return -1;
+ }
+
+ sButtonCount = buttonCodes.length;
+ }
+
+ return sButtonCount;
+ }
+
+ /**
+ * Returns an icon that can be used to represent the location of a button.
+ *
+ * @param context The context of the current activity
+ * @param keycode The keycode associated with the hardware button of interest
+ * @return A drawable representing the location of a button, or null if unavailable
+ */
+ @Nullable
+ public static Drawable getButtonIcon(@NonNull Context context, int keycode) {
+ ButtonInfo info = getButtonInfo(context, keycode);
+ if (info == null) {
+ return null;
+ }
+ return getButtonIconFromLocationZone(context, info.getLocationZone());
+ }
+
+ @VisibleForTesting
+ static RotateDrawable getButtonIconFromLocationZone(Context context, int locationZone) {
+ // To save memory for assets, we are using 4 icons to represent the 20+ possible
+ // configurations. These 4 base icons can be rotated to fit any configuration needed.
+ // id is the drawable id for the base icon
+ // degrees is the number of degrees the icon needs to be rotated to match the wanted
+ // position
+ int id;
+ int degrees;
+
+ switch (locationZone) {
+ // Round constants
+ case LOC_EAST:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = 0;
+ break;
+ case LOC_ENE:
+ case LOC_NE:
+ case LOC_NNE:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = -45;
+ break;
+ case LOC_NORTH:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = -90;
+ break;
+ case LOC_NNW:
+ case LOC_NW:
+ case LOC_WNW:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = -135;
+ break;
+ case LOC_WEST:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = 180;
+ break;
+ case LOC_WSW:
+ case LOC_SW:
+ case LOC_SSW:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = 135;
+ break;
+ case LOC_SOUTH:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = 90;
+ break;
+ case LOC_SSE:
+ case LOC_SE:
+ case LOC_ESE:
+ id = R.drawable.ic_cc_settings_button_e;
+ degrees = 45;
+ break;
+
+ // Rectangular constants
+ case LOC_LEFT_TOP:
+ id = R.drawable.ic_cc_settings_button_bottom;
+ degrees = 180;
+ break;
+ case LOC_LEFT_CENTER:
+ id = R.drawable.ic_cc_settings_button_center;
+ degrees = 180;
+ break;
+ case LOC_LEFT_BOTTOM:
+ id = R.drawable.ic_cc_settings_button_top;
+ degrees = 180;
+ break;
+ case LOC_RIGHT_TOP:
+ id = R.drawable.ic_cc_settings_button_top;
+ degrees = 0;
+ break;
+ case LOC_RIGHT_CENTER:
+ id = R.drawable.ic_cc_settings_button_center;
+ degrees = 0;
+ break;
+ case LOC_RIGHT_BOTTOM:
+ id = R.drawable.ic_cc_settings_button_bottom;
+ degrees = 0;
+ break;
+ case LOC_TOP_LEFT:
+ id = R.drawable.ic_cc_settings_button_top;
+ degrees = -90;
+ break;
+ case LOC_TOP_CENTER:
+ id = R.drawable.ic_cc_settings_button_center;
+ degrees = -90;
+ break;
+ case LOC_TOP_RIGHT:
+ id = R.drawable.ic_cc_settings_button_bottom;
+ degrees = -90;
+ break;
+ case LOC_BOTTOM_LEFT:
+ id = R.drawable.ic_cc_settings_button_bottom;
+ degrees = 90;
+ break;
+ case LOC_BOTTOM_CENTER:
+ id = R.drawable.ic_cc_settings_button_center;
+ degrees = 90;
+ break;
+ case LOC_BOTTOM_RIGHT:
+ id = R.drawable.ic_cc_settings_button_top;
+ degrees = 90;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected location zone");
+ }
+ RotateDrawable rotateIcon = new RotateDrawable();
+ rotateIcon.setDrawable(context.getDrawable(id));
+ rotateIcon.setFromDegrees(degrees);
+ rotateIcon.setToDegrees(degrees);
+ rotateIcon.setLevel(1);
+ return rotateIcon;
+ }
+
+ /**
+ * Returns a CharSequence that describes the placement location of a button. An example might be
+ * "Top right" or "Bottom".
+ *
+ * @param context The context of the current activity
+ * @param keycode The keycode associated with the hardware button of interest
+ * @return A CharSequence describing the placement location of the button
+ */
+ @NonNull
+ public static CharSequence getButtonLabel(@NonNull Context context, int keycode) {
+ // 4 length array where the index uses the standard quadrant counting system (minus 1 for
+ // 0 index)
+ int[] buttonsInQuadrantCount = new int[4];
+
+ // Retrieve ButtonInfo objects and count how many buttons are in a quadrant. This is only
+ // needed for round devices but will help us come up with friendly strings to show to the
+ // user.
+ // TODO(ahugh): We can cache quadrant counts to optimize. These values should be static for
+ // each
+ // device.
+ int[] buttonCodes = sButtonsProvider.getAvailableButtonKeyCodes(context);
+
+ if (buttonCodes == null) {
+ return null;
+ }
+
+ for (int key : buttonCodes) {
+ ButtonInfo info = getButtonInfo(context, key);
+
+ if (info != null) {
+ int quadrantIndex = getQuadrantIndex(info.getLocationZone());
+ if (quadrantIndex != -1) {
+ ++buttonsInQuadrantCount[quadrantIndex];
+ }
+ }
+ }
+
+ ButtonInfo info = getButtonInfo(context, keycode);
+ int quadrantIndex = (info != null ? getQuadrantIndex(info.getLocationZone()) : -1);
+ return info == null
+ ? null
+ : context.getString(
+ getFriendlyLocationZoneStringId(
+ info.getLocationZone(),
+ (quadrantIndex == -1 ? 0 : buttonsInQuadrantCount[quadrantIndex])));
+ }
+
+ /**
+ * Returns quadrant index if locationZone is for a round device. Follows the conventional
+ * quadrant system with the top right quadrant being 0, incrementing the index by 1 going
+ * counter-clockwise around.
+ */
+ private static int getQuadrantIndex(int locationZone) {
+ switch (locationZone) {
+ case LOC_ENE:
+ case LOC_NE:
+ case LOC_NNE:
+ return 0;
+ case LOC_NNW:
+ case LOC_NW:
+ case LOC_WNW:
+ return 1;
+ case LOC_WSW:
+ case LOC_SW:
+ case LOC_SSW:
+ return 2;
+ case LOC_SSE:
+ case LOC_SE:
+ case LOC_ESE:
+ return 3;
+
+ default:
+ return -1;
+ }
+ }
+
+ /**
+ * If the screen is round, there is special logic we use to determine the string that should
+ * show on the screen. Simple strings are broad descriptors like "top right". Detailed strings
+ * are narrow descriptors like, "top right, upper" 1) If there are exactly 2 buttons in a
+ * quadrant, use detailed strings to describe button locations. 2) Otherwise, use simple strings
+ * to describe the button locations.
+ *
+ * @param locationZone The location zone to get a string id for
+ * @param buttonsInQuadrantCount The number of buttons in the quadrant of the button
+ * @return The string id to use to represent this button zone
+ */
+ @VisibleForTesting
+ static int getFriendlyLocationZoneStringId(int locationZone, int buttonsInQuadrantCount) {
+ if (buttonsInQuadrantCount == 2) {
+ switch (locationZone) {
+ case LOC_ENE:
+ return R.string.buttons_round_top_right_lower;
+ case LOC_NE:
+ case LOC_NNE:
+ return R.string.buttons_round_top_right_upper;
+ case LOC_NNW:
+ case LOC_NW:
+ return R.string.buttons_round_top_left_upper;
+ case LOC_WNW:
+ return R.string.buttons_round_top_left_lower;
+ case LOC_ESE:
+ case LOC_SE:
+ return R.string.buttons_round_bottom_left_upper;
+ case LOC_SSE:
+ return R.string.buttons_round_bottom_left_lower;
+ case LOC_SSW:
+ return R.string.buttons_round_bottom_right_lower;
+ case LOC_SW:
+ case LOC_WSW:
+ return R.string.buttons_round_bottom_right_upper;
+ default: // fall out
+ }
+ }
+
+ // If we couldn't find a detailed string, or we need a simple string
+ switch (locationZone) {
+ // Round constants
+ case LOC_EAST:
+ return R.string.buttons_round_center_right;
+ case LOC_ENE:
+ case LOC_NE:
+ case LOC_NNE:
+ return R.string.buttons_round_top_right;
+ case LOC_NORTH:
+ return R.string.buttons_round_top_center;
+ case LOC_NNW:
+ case LOC_NW:
+ case LOC_WNW:
+ return R.string.buttons_round_top_left;
+ case LOC_WEST:
+ return R.string.buttons_round_center_left;
+ case LOC_WSW:
+ case LOC_SW:
+ case LOC_SSW:
+ return R.string.buttons_round_bottom_left;
+ case LOC_SOUTH:
+ return R.string.buttons_round_bottom_center;
+ case LOC_SSE:
+ case LOC_SE:
+ case LOC_ESE:
+ return R.string.buttons_round_bottom_right;
+
+ // Rectangular constants
+ case LOC_LEFT_TOP:
+ return R.string.buttons_rect_left_top;
+ case LOC_LEFT_CENTER:
+ return R.string.buttons_rect_left_center;
+ case LOC_LEFT_BOTTOM:
+ return R.string.buttons_rect_left_bottom;
+ case LOC_RIGHT_TOP:
+ return R.string.buttons_rect_right_top;
+ case LOC_RIGHT_CENTER:
+ return R.string.buttons_rect_right_center;
+ case LOC_RIGHT_BOTTOM:
+ return R.string.buttons_rect_right_bottom;
+ case LOC_TOP_LEFT:
+ return R.string.buttons_rect_top_left;
+ case LOC_TOP_CENTER:
+ return R.string.buttons_rect_top_center;
+ case LOC_TOP_RIGHT:
+ return R.string.buttons_rect_top_right;
+ case LOC_BOTTOM_LEFT:
+ return R.string.buttons_rect_bottom_left;
+ case LOC_BOTTOM_CENTER:
+ return R.string.buttons_rect_bottom_center;
+ case LOC_BOTTOM_RIGHT:
+ return R.string.buttons_rect_bottom_right;
+ default:
+ throw new IllegalArgumentException("Unexpected location zone");
+ }
+ }
+
+ /**
+ * For round devices, the location zone is defined using 16 points in a compass arrangement. If
+ * a button falls between anchor points, this method will return the closest anchor.
+ *
+ * <p>For rectangular devices, the location zone is defined by splitting each side into thirds.
+ * If a button falls anywhere within a zone, the method will return that zone. The constants for
+ * these zones are named LOC_[side in question]_[which third is affected]. E.g. LOC_TOP_RIGHT
+ * would refer to the right third of the top side of the device.
+ */
+ @VisibleForTesting
+ /* package */ static int getLocationZone(
+ boolean isRound, Point screenSize, float screenLocationX, float screenLocationY) {
+ if (screenLocationX == Float.MAX_VALUE || screenLocationY == Float.MAX_VALUE) {
+ return LOC_UNKNOWN;
+ }
+
+ return isRound
+ ? getLocationZoneRound(screenSize, screenLocationX, screenLocationY)
+ : getLocationZoneRectangular(screenSize, screenLocationX, screenLocationY);
+ }
+
+ private static int getLocationZoneRound(
+ Point screenSize, float screenLocationX, float screenLocationY) {
+ // Convert screen coordinate to Cartesian coordinate
+ float cartesianX = screenLocationX - screenSize.x / 2;
+ float cartesianY = screenSize.y / 2 - screenLocationY;
+
+ // Use polar coordinates to figure out which zone the point is in
+ double angle = Math.atan2(cartesianY, cartesianX);
+
+ // Convert angle to all positive values
+ if (angle < 0) {
+ angle += 2 * Math.PI;
+ }
+
+ // Return the associated section rounded to the nearest anchor.
+ // Using some clever math tricks and enum declaration, we can reduce this calculation
+ // down to a single formula that converts angle to enum value.
+ return Math.round((float) (angle / (Math.PI / 8))) % LOC_ROUND_COUNT;
+ }
+
+ private static int getLocationZoneRectangular(
+ Point screenSize, float screenLocationX, float screenLocationY) {
+ // Calculate distance to each edge.
+ float deltaFromLeft = screenLocationX;
+ float deltaFromRight = screenSize.x - screenLocationX;
+ float deltaFromTop = screenLocationY;
+ float deltaFromBottom = screenSize.y - screenLocationY;
+ float minDelta =
+ Math.min(
+ deltaFromLeft,
+ Math.min(deltaFromRight, Math.min(deltaFromTop, deltaFromBottom)));
+
+ // Prioritize ties to left and right sides of watch since they're more likely to be placed
+ // on the side. Buttons directly on the corner are not accounted for with this API.
+ if (minDelta == deltaFromLeft) {
+ // Left is the primary side
+ switch (whichThird(screenSize.y, screenLocationY)) {
+ case 0:
+ return LOC_LEFT_TOP;
+ case 1:
+ return LOC_LEFT_CENTER;
+ default:
+ return LOC_LEFT_BOTTOM;
+ }
+ } else if (minDelta == deltaFromRight) {
+ // Right is primary side
+ switch (whichThird(screenSize.y, screenLocationY)) {
+ case 0:
+ return LOC_RIGHT_TOP;
+ case 1:
+ return LOC_RIGHT_CENTER;
+ default:
+ return LOC_RIGHT_BOTTOM;
+ }
+ } else if (minDelta == deltaFromTop) {
+ // Top is primary side
+ switch (whichThird(screenSize.x, screenLocationX)) {
+ case 0:
+ return LOC_TOP_LEFT;
+ case 1:
+ return LOC_TOP_CENTER;
+ default:
+ return LOC_TOP_RIGHT;
+ }
+ } else /* if (minDelta == deltaFromBottom) */ {
+ // Bottom is primary side
+ switch (whichThird(screenSize.x, screenLocationX)) {
+ case 0:
+ return LOC_BOTTOM_LEFT;
+ case 1:
+ return LOC_BOTTOM_CENTER;
+ default:
+ return LOC_BOTTOM_RIGHT;
+ }
+ }
+ }
+
+ // Returns 0, 1, or 2 which correspond to the index of the third the screen point lies in
+ // from 'left to right' or 'top to bottom'.
+ private static int whichThird(float screenLength, float screenLocation) {
+ if (screenLocation <= screenLength / 3) {
+ return 0;
+ } else if (screenLocation <= screenLength * 2 / 3) {
+ return 1;
+ } else {
+ return 2;
+ }
+ }
+
+ private static boolean isLeftyModeEnabled(Context context) {
+ return Settings.System.getInt(
+ context.getContentResolver(),
+ Settings.System.USER_ROTATION,
+ Surface.ROTATION_0)
+ == Surface.ROTATION_180;
+ }
+
+ /** Metadata for a specific button. */
+ public static final class ButtonInfo {
+ private final int mKeycode;
+ private final float mX;
+ private final float mY;
+
+ /**
+ * The location zone of the button as defined in the {@link #getButtonInfo(Context, int)}
+ * method. The intended use is to help developers attach a friendly String to the button
+ * location. This value is LOC_UNKNOWN if the information is not available.
+ */
+ private final int mLocationZone;
+
+ /**
+ * Gets the keycode this {@code ButtonInfo} provides information for.
+ *
+ * @return The keycode this {@code ButtonInfo} provides information for
+ */
+ public int getKeycode() {
+ return mKeycode;
+ }
+
+ /** The x coordinate of the button in screen coordinates. */
+ public float getX() {
+ return mX;
+ }
+
+ /** The y coordinate of the button in screen coordinates. */
+ public float getY() {
+ return mY;
+ }
+
+ /** The location zone of the button (e.g. LOC_EAST) */
+ public int getLocationZone() {
+ return mLocationZone;
+ }
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @VisibleForTesting
+ public ButtonInfo(int keycode, float x, float y, int locationZone) {
+ this.mKeycode = keycode;
+ this.mX = x;
+ this.mY = y;
+ this.mLocationZone = locationZone;
+ }
+ }
+}
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/WearableButtonsProvider.java b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtonsProvider.java
new file mode 100644
index 0000000..5a68920
--- /dev/null
+++ b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtonsProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 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 androidx.wear.input;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A provider interface to allow {@link WearableButtons} to query for information on the device's
+ * buttons from the platform. This exists to allow for the button provider to be switched out for
+ * testing, for example, by using {@link androidx.wear.input.testing.TestWearableButtonsProvider}.
+ */
+public interface WearableButtonsProvider {
+ /**
+ * Returns a bundle containing the metadata of a specific button. Currently, only location is
+ * supported. Use with {@link com.google.android.wearable.input.WearableInputDevice#X_KEY} and
+ * {@link com.google.android.wearable.input.WearableInputDevice#Y_KEY}. The key will not be
+ * present if the information is not available for the requested keycode.
+ *
+ * <p>The location returned is a Cartesian coordinate where the bottom left corner of the screen
+ * is the origin. The unit of measurement is in pixels. The coordinates do not take rotation
+ * into account and assume that the device is in the standard upright position.
+ *
+ * @param context The context of the current activity
+ * @param keycode The keycode associated with the hardware button of interest
+ * @return A {@link Bundle} containing the metadata for the given keycode
+ */
+ @NonNull
+ Bundle getButtonInfo(@NonNull Context context, int keycode);
+
+ /**
+ * Get the keycodes of available hardware buttons on device. This function based on key's
+ * locations from system property. This count includes the primary stem key as well as any
+ * secondary stem keys available.
+ *
+ * @param context The context of the current activity
+ * @return An int array of available button keycodes, or null if no keycodes could be read.
+ */
+ @Nullable
+ int[] getAvailableButtonKeyCodes(@NonNull Context context);
+}
diff --git a/wear/wear-input/src/main/res/color/button_icon_color.xml b/wear/wear-input/src/main/res/color/button_icon_color.xml
new file mode 100644
index 0000000..cc6e02b
--- /dev/null
+++ b/wear/wear-input/src/main/res/color/button_icon_color.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/textColorPrimary" android:state_enabled="true" />
+ <item android:color="?android:colorButtonNormal" android:state_enabled="false" />
+</selector>
diff --git a/wear/wear-input/src/main/res/drawable/action_item_background.xml b/wear/wear-input/src/main/res/drawable/action_item_background.xml
new file mode 100644
index 0000000..22d254d
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/action_item_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <color android:color="?android:attr/colorControlHighlight" />
+ </item>
+</ripple>
diff --git a/wear/wear-input/src/main/res/drawable/action_item_icon_background.xml b/wear/wear-input/src/main/res/drawable/action_item_icon_background.xml
new file mode 100644
index 0000000..b10a515
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/action_item_icon_background.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="?android:attr/colorForeground"/>
+</shape>
diff --git a/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_bottom.xml b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_bottom.xml
new file mode 100644
index 0000000..cdbb37a
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_bottom.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="@color/button_icon_color"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M20.8,19.1V4.9C20.8,3.8 20,3 18.9,3H4.7C3.6,3 2.8,3.8 2.8,4.9v14.1c0,1.1 0.8,1.9 1.9,1.9h14.1C20,21 20.8,20.2 20.8,19.1zM18.9,19.1H4.7V4.9h14.1V19.1z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M24,15.9v2.6c0,0.4 -0.3,0.6 -0.6,0.6h-1.3v-3.9h1.3C23.7,15.2 24,15.5 24,15.9z" />
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_center.xml b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_center.xml
new file mode 100644
index 0000000..364c77d
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_center.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="@color/button_icon_color"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18.9,3H4.7C3.6,3 2.8,3.8 2.8,4.9v14.1c0,1.1 0.8,1.9 1.9,1.9h14.1c1.1,0 1.9,-0.8 1.9,-1.9V4.9C20.8,3.8 20,3 18.9,3zM18.9,19.1H4.7V4.9h14.1V19.1z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M23.4,14h-1.3v-3.9h1.3c0.4,0 0.6,0.3 0.6,0.6v2.6C24,13.7 23.7,14 23.4,14z" />
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_e.xml b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_e.xml
new file mode 100644
index 0000000..df85799
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_e.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="@color/button_icon_color"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,21c5,0 9,-4 9,-9s-4,-9 -9,-9c-5,0 -9,4 -9,9S7,21 12,21zM12,5c3.9,0 7,3.1 7,7s-3.1,7 -7,7s-7,-3.1 -7,-7S8.1,5 12,5z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M22.2,9H24v6h-1.8V9z" />
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_top.xml b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_top.xml
new file mode 100644
index 0000000..cc5c342
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_cc_settings_button_top.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="@color/button_icon_color"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18.9,3H4.7C3.6,3 2.8,3.8 2.8,4.9v14.1c0,1.1 0.8,1.9 1.9,1.9h14.1c1.1,0 1.9,-0.8 1.9,-1.9V4.9C20.8,3.8 20,3 18.9,3zM18.9,19.1H4.7V4.9h14.1V19.1z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M23.4,8.8h-1.3V4.9h1.3c0.4,0 0.6,0.3 0.6,0.6v2.6C24,8.5 23.7,8.8 23.4,8.8z" />
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_expand_less_white_22.xml b/wear/wear-input/src/main/res/drawable/ic_expand_less_white_22.xml
new file mode 100644
index 0000000..2dbcf36
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_expand_less_white_22.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_more_horiz_24dp_wht.xml b/wear/wear-input/src/main/res/drawable/ic_more_horiz_24dp_wht.xml
new file mode 100644
index 0000000..723b606
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_more_horiz_24dp_wht.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#ffffff"
+ android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/ic_more_vert_24dp_wht.xml b/wear/wear-input/src/main/res/drawable/ic_more_vert_24dp_wht.xml
new file mode 100644
index 0000000..fbb5018
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/ic_more_vert_24dp_wht.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#ffffffff"
+ android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>
diff --git a/wear/wear-input/src/main/res/drawable/preference_wrapped_icon.xml b/wear/wear-input/src/main/res/drawable/preference_wrapped_icon.xml
new file mode 100644
index 0000000..789d5ee
--- /dev/null
+++ b/wear/wear-input/src/main/res/drawable/preference_wrapped_icon.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <item>
+ <shape android:shape="oval" android:tint="?android:colorPrimary">
+ <solid android:color="@android:color/white" />
+ <size android:width="40dp" android:height="40dp" />
+ </shape>
+ </item>
+ <item
+ android:id="@+id/nested_icon"
+ android:drawable="@android:color/transparent"
+ android:height="24dp"
+ android:width="24dp"
+ android:gravity="center" />
+</layer-list>
diff --git a/wear/wear-input/src/main/res/values/color.xml b/wear/wear-input/src/main/res/values/color.xml
new file mode 100644
index 0000000..cdac867
--- /dev/null
+++ b/wear/wear-input/src/main/res/values/color.xml
@@ -0,0 +1,65 @@
+<resources>
+ <color name="white">@android:color/white</color>
+ <color name="black">@android:color/black</color>
+
+ <!-- Blue A400 -->
+ <color name="blue_a400">#2979FF</color>
+ <!-- Blue 800 -->
+ <color name="dark_blue">#1565C0</color>
+ <!-- Green A700 -->
+ <color name="green">#00C853</color>
+ <!-- Grey 400 -->
+ <color name="light_grey">#bdbdbd</color>
+ <!-- Grey 600 -->
+ <color name="grey">#757575</color>
+ <!-- Grey 800 -->
+ <color name="dark_grey">#424242</color>
+ <!-- Red A200 -->
+ <color name="red_a200">#FF5252</color>
+ <!-- Red 700 -->
+ <color name="dark_red">#D32F2F</color>
+ <!-- Amber 900 -->
+ <color name="orange">#FF6D3F</color>
+
+ <color name="semitransparent_grey">#66777777</color>
+
+ <color name="dismiss_overlay_bg">#80000000</color>
+ <color name="dismiss_close">#FF5151</color>
+ <color name="dismiss_close_pressed">#B83120</color>
+
+ <!-- Light Blue 700 -->
+ <color name="circular_button_normal">#0288D1</color>
+ <!-- Light Blue 900 -->
+ <color name="circular_button_pressed">#01579B</color>
+ <color name="circular_button_disabled">@color/grey</color>
+ <color name="primary_text_light">@color/dark_grey</color>
+ <!-- Grey 500 -->
+ <color name="secondary_text_light">#9E9E9E</color>
+ <!-- Grey 300 -->
+ <color name="disabled_text_light">#E0E0E0</color>
+
+ <color name="primary_text_dark">@color/white</color>
+ <color name="ambient_mode_text">@color/primary_text_dark</color>
+ <!-- Grey 300 -->
+ <color name="card_default_background">#E0E0E0</color>
+
+ <!-- 86% Black -->
+ <color name="black_86p">#DE000000</color>
+ <!-- 54% Black -->
+ <color name="black_54p">#8A000000</color>
+
+ <!-- 30% Black -->
+ <color name="action_button_background">#4D000000</color>
+
+ <color name="dialog_background">#414141</color>
+ <color name="dialog_shade_background">#606060</color>
+
+ <!-- Default colors to be used by the spinner if none are provided -->
+ <array name="progress_spinner_sequence">
+ <item>#FFF4B400</item>
+ <item>#FFDB4437</item>
+ <item>#FF4285F4</item>
+ <item>#FF0F9D58</item>
+ </array>
+
+</resources>
diff --git a/wear/wear-input/src/main/res/values/strings.xml b/wear/wear-input/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4536847
--- /dev/null
+++ b/wear/wear-input/src/main/res/values/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- String describing a button location on a round device when the button is in the top center position [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_center">Top</string>
+ <!-- String describing a button location on a round device when the button is in the top right position [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_right">Top right</string>
+ <!-- String describing a button location on a round device when the button is in the center right position [CHAR LIMIT=35] -->
+ <string name="buttons_round_center_right">Center right</string>
+ <!-- String describing a button location on a round device when the button is in the bottom right position [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_right">Bottom right</string>
+ <!-- String describing a button location on a round device when the button is in the bottom center position [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_center">Bottom</string>
+ <!-- String describing a button location on a round device when the button is in the bottom left position [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_left">Bottom left</string>
+ <!-- String describing a button location on a round device when the button is in the center left position [CHAR LIMIT=35] -->
+ <string name="buttons_round_center_left">Center left</string>
+ <!-- String describing a button location on a round device when the button is in the top left position [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_left">Top left</string>
+ <!-- String describing a button location on a round device when the button between 12 o'clock and halfway between 1 and 2 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_right_upper">Top right, upper</string>
+ <!-- String describing a button location on a round device when the button between halfway between 1 and 2 o'clock and 3 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_right_lower">Top right, lower</string>
+ <!-- String describing a button location on a round device when the button between 3 o'clock and halfway between 4 and 5 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_right_upper">Bottom right, upper</string>
+ <!-- String describing a button location on a round device when the button between halfway between 4 and 5 o'clock and 6 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_right_lower">Bottom right, lower</string>
+ <!-- String describing a button location on a round device when the button between 6 o'clock and halfway between 7 and 8 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_left_lower">Bottom left, lower</string>
+ <!-- String describing a button location on a round device when the button between halfway between 7 and 8 o'clock and 9 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_bottom_left_upper">Bottom left, upper</string>
+ <!-- String describing a button location on a round device when the button between 9 o'clock and halfway between 10 and 11 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_left_lower">Top left, lower</string>
+ <!-- String describing a button location on a round device when the button between halfway between 10 and 11 o'clock and 12 o'clock [CHAR LIMIT=35] -->
+ <string name="buttons_round_top_left_upper">Top left, upper</string>
+ <!-- String describing a button location on a square device when the button is on the left side near the top corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_left_top">Top, left side</string>
+ <!-- String describing a button location on a square device when the button is on the left side near the center [CHAR LIMIT=35] -->
+ <string name="buttons_rect_left_center">Center left</string>
+ <!-- String describing a button location on a square device when the button is on the left side near the bottom corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_left_bottom">Bottom, left side</string>
+ <!-- String describing a button location on a square device when the button is on the right side near the top corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_right_top">Top, right side</string>
+ <!-- String describing a button location on a square device when the button is on the right side near the center [CHAR LIMIT=35] -->
+ <string name="buttons_rect_right_center">Center right</string>
+ <!-- String describing a button location on a square device when the button is on the right side near the bottom corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_right_bottom">Bottom, right side</string>
+ <!-- String describing a button location on a square device when the button is on the top side near the left corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_top_left">Top left</string>
+ <!-- String describing a button location on a square device when the button is on the top side near the center [CHAR LIMIT=35] -->
+ <string name="buttons_rect_top_center">Top</string>
+ <!-- String describing a button location on a square device when the button is on the top side near the right corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_top_right">Top right</string>
+ <!-- String describing a button location on a square device when the button is on the bottom side near the left corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_bottom_left">Bottom left</string>
+ <!-- String describing a button location on a square device when the button is on the bottom side near the center [CHAR LIMIT=35] -->
+ <string name="buttons_rect_bottom_center">Bottom</string>
+ <!-- String describing a button location on a square device when the button is on the bottom side near the right corner [CHAR LIMIT=35] -->
+ <string name="buttons_rect_bottom_right">Bottom right</string>
+</resources>
diff --git a/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java b/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java
new file mode 100644
index 0000000..99b4c4a
--- /dev/null
+++ b/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2020 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 androidx.wear.input;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.RotateDrawable;
+import android.os.Build;
+import android.provider.Settings;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.wear.R;
+import androidx.wear.input.testing.TestWearableButtonsProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadows.ShadowDrawable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Unit tests for {@link WearableButtons}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(sdk = Build.VERSION_CODES.P)
+public class WearableButtonsTest {
+ private final Point mScreenSize = new Point(480, 480);
+
+ @Test
+ public void testExactPoints_round() {
+ // Positions were calculated using the following formula:
+ // ScreenCoordinate.x = r*(1+cos(a))
+ // ScreenCoordinate.y = r*(1-sin(a))
+ // Angle = pi/8 * N, where N was the cardinal position
+ // r = 240 because the screen is 480x480
+ // Wolfram Alpha was helpful: x=240*(1+cos(a)), y=240*(1-sin(a)), a=pi/8.0
+
+ assertEquals(
+ WearableButtons.LOC_EAST,
+ WearableButtons.getLocationZone(true, mScreenSize, 480, 240));
+ assertEquals(
+ WearableButtons.LOC_ENE,
+ WearableButtons.getLocationZone(true, mScreenSize, 461.731f, 148.156f));
+ assertEquals(
+ WearableButtons.LOC_NE,
+ WearableButtons.getLocationZone(true, mScreenSize, 409.706f, 70.294f));
+ assertEquals(
+ WearableButtons.LOC_NNE,
+ WearableButtons.getLocationZone(true, mScreenSize, 331.844f, 18.269f));
+ assertEquals(
+ WearableButtons.LOC_NORTH,
+ WearableButtons.getLocationZone(true, mScreenSize, 240, 0));
+ assertEquals(
+ WearableButtons.LOC_NNW,
+ WearableButtons.getLocationZone(true, mScreenSize, 148.156f, 18.269f));
+ assertEquals(
+ WearableButtons.LOC_NW,
+ WearableButtons.getLocationZone(true, mScreenSize, 70.294f, 70.294f));
+ assertEquals(
+ WearableButtons.LOC_WNW,
+ WearableButtons.getLocationZone(true, mScreenSize, 18.269f, 148.156f));
+ assertEquals(
+ WearableButtons.LOC_WEST,
+ WearableButtons.getLocationZone(true, mScreenSize, 0, 240));
+ assertEquals(
+ WearableButtons.LOC_WSW,
+ WearableButtons.getLocationZone(true, mScreenSize, 18.269f, 331.844f));
+ assertEquals(
+ WearableButtons.LOC_SW,
+ WearableButtons.getLocationZone(true, mScreenSize, 70.294f, 409.706f));
+ assertEquals(
+ WearableButtons.LOC_SSW,
+ WearableButtons.getLocationZone(true, mScreenSize, 148.156f, 461.731f));
+ assertEquals(
+ WearableButtons.LOC_SOUTH,
+ WearableButtons.getLocationZone(true, mScreenSize, 240, 480));
+ assertEquals(
+ WearableButtons.LOC_SSE,
+ WearableButtons.getLocationZone(true, mScreenSize, 331.844f, 461.731f));
+ assertEquals(
+ WearableButtons.LOC_SE,
+ WearableButtons.getLocationZone(true, mScreenSize, 409.706f, 409.706f));
+ assertEquals(
+ WearableButtons.LOC_ESE,
+ WearableButtons.getLocationZone(true, mScreenSize, 461.731f, 331.844f));
+ }
+
+ @Test
+ public void testNonExactPoints_round() {
+ // Positions were randomly determined based on #testExactPoints calculations
+ // 1 degree in radians = Math.PI / 180;
+ // Using formula from #testExactPoints, add and subtract X degrees from each exact point and
+ // test edge cases
+ // Wolfram Alpha helps again: x = 240*(1+cos(a)), y=240*(1-sin(a)), a=(15.0*pi/8.0)+pi/180
+
+ // Exact +1 degree
+ assertEquals(
+ WearableButtons.LOC_ESE,
+ WearableButtons.getLocationZone(true, mScreenSize, 463.3f, 327.96f));
+ // Exact -1 degree
+ assertEquals(
+ WearableButtons.LOC_ESE,
+ WearableButtons.getLocationZone(true, mScreenSize, 460.094f, 335.7f));
+
+ // Exact +5 degrees
+ assertEquals(
+ WearableButtons.LOC_WNW,
+ WearableButtons.getLocationZone(true, mScreenSize, 16.7f, 152.04f));
+ // Exact -5 degrees
+ assertEquals(
+ WearableButtons.LOC_WNW,
+ WearableButtons.getLocationZone(true, mScreenSize, 19.906f, 144.3f));
+
+ // Exactly between SE and ESE
+ assertEquals(
+ WearableButtons.LOC_ESE,
+ WearableButtons.getLocationZone(true, mScreenSize, 439.553f, 373.337f));
+
+ // Exactly between SSE and SE
+ assertEquals(
+ WearableButtons.LOC_SE,
+ WearableButtons.getLocationZone(true, mScreenSize, 373.337f, 439.553f));
+ }
+
+ @Test
+ public void testEdgePoints_rect() {
+ assertEquals(
+ WearableButtons.LOC_LEFT_TOP,
+ WearableButtons.getLocationZone(false, mScreenSize, 0, 160));
+ assertEquals(
+ WearableButtons.LOC_LEFT_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 0, 320));
+ assertEquals(
+ WearableButtons.LOC_LEFT_BOTTOM,
+ WearableButtons.getLocationZone(false, mScreenSize, 0, 480));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_TOP,
+ WearableButtons.getLocationZone(false, mScreenSize, 480, 160));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 480, 320));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_BOTTOM,
+ WearableButtons.getLocationZone(false, mScreenSize, 480, 480));
+ assertEquals(
+ WearableButtons.LOC_TOP_LEFT,
+ WearableButtons.getLocationZone(false, mScreenSize, 160, 0));
+ assertEquals(
+ WearableButtons.LOC_TOP_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 320, 0));
+ assertEquals(
+ WearableButtons.LOC_TOP_RIGHT,
+ WearableButtons.getLocationZone(false, mScreenSize, 479, 0));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_LEFT,
+ WearableButtons.getLocationZone(false, mScreenSize, 160, 480));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 320, 480));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_RIGHT,
+ WearableButtons.getLocationZone(false, mScreenSize, 479, 480));
+ }
+
+ @Test
+ public void testNonEdgePoints_rect() {
+ assertEquals(
+ WearableButtons.LOC_LEFT_TOP,
+ WearableButtons.getLocationZone(false, mScreenSize, 50, 100));
+ assertEquals(
+ WearableButtons.LOC_LEFT_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 80, 300));
+ assertEquals(
+ WearableButtons.LOC_LEFT_BOTTOM,
+ WearableButtons.getLocationZone(false, mScreenSize, 100, 350));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_TOP,
+ WearableButtons.getLocationZone(false, mScreenSize, 460, 120));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 450, 270));
+ assertEquals(
+ WearableButtons.LOC_RIGHT_BOTTOM,
+ WearableButtons.getLocationZone(false, mScreenSize, 430, 400));
+ assertEquals(
+ WearableButtons.LOC_TOP_LEFT,
+ WearableButtons.getLocationZone(false, mScreenSize, 130, 20));
+ assertEquals(
+ WearableButtons.LOC_TOP_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 280, 30));
+ assertEquals(
+ WearableButtons.LOC_TOP_RIGHT,
+ WearableButtons.getLocationZone(false, mScreenSize, 429, 40));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_LEFT,
+ WearableButtons.getLocationZone(false, mScreenSize, 140, 470));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_CENTER,
+ WearableButtons.getLocationZone(false, mScreenSize, 290, 460));
+ assertEquals(
+ WearableButtons.LOC_BOTTOM_RIGHT,
+ WearableButtons.getLocationZone(false, mScreenSize, 439, 450));
+ }
+
+ @Test
+ public void testSimpleStrings() {
+ assertEquals(
+ R.string.buttons_round_bottom_right,
+ WearableButtons.getFriendlyLocationZoneStringId(WearableButtons.LOC_ESE, 1));
+ assertEquals(
+ R.string.buttons_round_bottom_left_upper,
+ WearableButtons.getFriendlyLocationZoneStringId(WearableButtons.LOC_ESE, 2));
+ assertEquals(
+ R.string.buttons_rect_bottom_left,
+ WearableButtons.getFriendlyLocationZoneStringId(
+ WearableButtons.LOC_BOTTOM_LEFT, 1));
+ assertEquals(
+ R.string.buttons_rect_bottom_left,
+ WearableButtons.getFriendlyLocationZoneStringId(
+ WearableButtons.LOC_BOTTOM_LEFT, 2));
+ }
+
+ @Test
+ public void testSimpleIcons() {
+ testRotateDrawable(WearableButtons.LOC_ESE, R.drawable.ic_cc_settings_button_e, 45);
+ testRotateDrawable(WearableButtons.LOC_WNW, R.drawable.ic_cc_settings_button_e, -135);
+ testRotateDrawable(
+ WearableButtons.LOC_BOTTOM_LEFT, R.drawable.ic_cc_settings_button_bottom, 90);
+ testRotateDrawable(
+ WearableButtons.LOC_TOP_CENTER, R.drawable.ic_cc_settings_button_center, -90);
+ testRotateDrawable(WearableButtons.LOC_TOP_LEFT, R.drawable.ic_cc_settings_button_top, -90);
+ }
+
+ @Test
+ public void testGetButtonsRighty() {
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2, 3, 4));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+
+ setLeftyModeEnabled(false);
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ assertEquals(1, info.getX(), 1.0e-7);
+ assertEquals(2, info.getY(), 1.0e-7);
+ }
+
+ @Test
+ public void testGetButtonsLefty() {
+ setLeftyModeEnabled(true);
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2, 3, 4));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ assertEquals(3, info.getX(), 1.0e-7);
+ assertEquals(4, info.getY(), 1.0e-7);
+ }
+
+ @Test
+ public void testGetButtonsLeftyNoData() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(480);
+ Shadows.shadowOf(display).setHeight(480);
+
+ setLeftyModeEnabled(true);
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(context, KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ assertEquals(479, info.getX(), 1.0e-7); // == 480 - 1
+ assertEquals(478, info.getY(), 1.0e-7); // == 480 - 2
+ }
+
+ private void testRotateDrawable(
+ int locationZone, int expectedDrawableId, int expectedDegreeRotation) {
+ RotateDrawable rotateDrawable =
+ WearableButtons.getButtonIconFromLocationZone(
+ ApplicationProvider.getApplicationContext(), locationZone);
+
+ // We need Robolectric to pull out the underlying resource ID.
+ ShadowDrawable rotateDrawableShadow = Shadows.shadowOf(rotateDrawable.getDrawable());
+
+ assertEquals(rotateDrawableShadow.getCreatedFromResId(), expectedDrawableId);
+ assertEquals(expectedDegreeRotation, rotateDrawable.getFromDegrees(), .001);
+ }
+
+ private void setLeftyModeEnabled(boolean enabled) {
+ Settings.System.putInt(
+ ApplicationProvider.getApplicationContext().getContentResolver(),
+ Settings.System.USER_ROTATION,
+ enabled ? Surface.ROTATION_180 : Surface.ROTATION_0);
+ }
+}
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 2cf341a..0ecccdf 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -41,6 +41,7 @@
name = "Android Wear Support UI"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR
+ mavenVersion = LibraryVersions.WEAR
inceptionYear = "2016"
description = "Android Wear Support UI"
}