├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tapwithus │ │ └── tapsdk │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tapwithus │ │ │ └── tapsdk │ │ │ ├── MainActivity.java │ │ │ ├── RecyclerViewAdapter.java │ │ │ ├── TapListItem.java │ │ │ ├── TapListItemOnClickListener.java │ │ │ └── TestPacket.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── circle_empty.xml │ │ ├── circle_filled.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── list_row.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── tapwithus │ └── tapsdk │ └── ExampleUnitTest.java ├── build.gradle ├── buildWithCompileDependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── Secret stuff ├── publish-module.gradle └── publish-root.gradle ├── settings.gradle ├── tap-android-sdk.zip ├── tap-android-sdk ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tapwithus │ │ └── sdk │ │ ├── FeatureCompatibility.java │ │ ├── FeatureVersionSupport.java │ │ ├── ListenerManager.java │ │ ├── NotifyAction.java │ │ ├── TapListener.java │ │ ├── TapSdk.java │ │ ├── TapSdkFactory.java │ │ ├── VersionRange.java │ │ ├── airmouse │ │ └── AirMousePacket.java │ │ ├── bluetooth │ │ ├── BluetoothListener.java │ │ ├── BluetoothManager.java │ │ ├── ErrorStrings.java │ │ ├── ExamplePacket.java │ │ ├── GattError.java │ │ ├── Packet.java │ │ ├── TapBluetoothListener.java │ │ ├── TapBluetoothManager.java │ │ ├── callbacks │ │ │ ├── OnCompletionListener.java │ │ │ ├── OnErrorListener.java │ │ │ └── OnNotFoundListener.java │ │ └── operations │ │ │ ├── BaseCharacteristicOperation.java │ │ │ ├── CharacteristicOperation.java │ │ │ ├── CharacteristicReadOperation.java │ │ │ ├── CharacteristicWriteOperation.java │ │ │ ├── DescriptorOperation.java │ │ │ ├── DescriptorReadOperation.java │ │ │ ├── DescriptorWriteOperation.java │ │ │ ├── DisconnectOperation.java │ │ │ ├── DiscoverServicesOperation.java │ │ │ ├── GattExecutor.java │ │ │ ├── GattOperation.java │ │ │ ├── GattOperationBundle.java │ │ │ ├── OperationType.java │ │ │ ├── RefreshOperation.java │ │ │ └── SetNotificationOperation.java │ │ ├── haptic │ │ └── HapticPacket.java │ │ ├── mode │ │ ├── Point3.java │ │ ├── RawSensorData.java │ │ ├── RawSensorDataParser.java │ │ ├── RawSensorDataParserListener │ │ ├── SensorSensitivity.java │ │ ├── TapInputMode.java │ │ └── TapXRState.java │ │ ├── mouse │ │ └── MousePacket.java │ │ └── tap │ │ ├── FeatureVersionRange.java │ │ ├── Tap.java │ │ └── TapCache.java │ └── res │ └── values │ └── strings.xml └── tap-unity ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tapwithus │ │ └── tapunity │ │ └── TapUnityAdapter.java │ └── res │ └── values │ └── strings.xml └── unityLibs └── classes.jar /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | /local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/vcs.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | .idea 47 | 48 | # Keystore files 49 | # Uncomment the following line if you do not want to check your keystore files in. 50 | #*.jks 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | 55 | # Google Services (e.g. APIs or Firebase) 56 | google-services.json 57 | 58 | # Freeline 59 | freeline.py 60 | freeline/ 61 | freeline_project_description.json 62 | 63 | app/release/ 64 | 65 | // do not want to store our secret gpg here 66 | *.gpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TAP Official Android SDK 2 | 3 | Updates 4 | ======= 5 | 6 | July 2024 - Added **TAPXR Gestures** (See below). 7 | 8 | 9 | What Is This? 10 | ============= 11 | TAP Android SDK allows you to build an Android app that can receive inputs from TAP devices, 12 | In a way that each tap is being interpreted as an array of fingers that are tapped, or as a binary combination integer (explanation follows), thus allowing the TAP device to act as a controller for your app! 13 | 14 | Getting started 15 | =============== 16 | To add TAP SDK library to your project: 17 | - Make sure you have mavenCentral in your Gradle repositories. 18 | - Add the following Gradle dependency to your build.gradle: 19 | ```Groovy 20 | implementation 'io.github.tapwithus:tap-android-sdk:0.3.6' 21 | ``` 22 | 23 | Getting instance of TapSdk 24 | ========================== 25 | The entry point of TAP SDK is the `com.tapwithus.sdk.TapSdk` class. 26 | 27 | If your application might be using the default `TapSdk` instance, a `TapSdk` single instance can be retrieved simply by using the `TapSdkFactory` helper class by calling `TapSdkFactory.getDefault(context)`. This helper method will return a single instance of `TapSdk` relatively to the passed context lifecycle. 28 | 29 | If an alternative `TapSdk` instance is required, or if you might be using a dependency injection framework where custom scopes is implemented, call the following commands instead, passing the appropriate arguments: 30 | ```Java 31 | BluetoothManager bluetoothManager = new BluetoothManager(context.getApplicationContext(), BluetoothAdapter.getDefaultAdapter()); 32 | TapBluetoothManager tapBluetoothManager = new TapBluetoothManager(bluetoothManager); 33 | TapSdk sdk = new TapSdk(tapBluetoothManager); 34 | ``` 35 | 36 | Key features 37 | ============ 38 | ##### Input Modes 39 | As stated in the official [Tap BLE API Documentation](https://www.tapwithus.com/api), once you turn ON the TAP device, by default, it will be booted into Text Mode, meaning that the TAP device functions as a bluetooth keyboard, every recognized tap will be mapped to a letter, and __no input data will be sent to the SDK__. 40 | 41 | When using the SDK, it is required to get the input data for a specific TAP device. In order to achieve this goal, after a connection with the TAP been established, we need to switch the TAP device to Controller Mode. In addition, it is important we switch back to Text Mode once the application goes to background, so the regular TAP behaviour will be restored. 42 | 43 | To simplify the process TAP SDK will perform the needed actions in order to correctly connect and switch between Modes _automatically_, __so you don't have to.__ 44 | 45 | Two additional modes to Text and Controller : 46 | Controller with Mouse HID: Behaves as a controller, but also allow the TAP to control the mouse cursor). 47 | Raw Sensor Mode: Streams raw sensor data (from Gyro and Accelerometer sensors on TAP). More or that later... 48 | 49 | What now? 50 | ========= 51 | Once a `TapSdk` instance been instantiated, TAP SDK will start doing his magic, by performing the following actions: 52 | * Auto establishing connections with paired TAP devices. 53 | * After a connection with a TAP device been established successfully, switching it to Controller Mode. 54 | * When the application goes to background, switching back to Text Mode. 55 | * When the application returns from background, switching back to Controller Mode. 56 | 57 | The only thing you need to take care of is to register for the necessary events. 58 | 59 | Registering TapListener 60 | ======================= 61 | `com.tapwithus.sdk.TapListener` is an interface, describing the various callbacks you can get back from `TapSdk`. 62 | 63 | ```Java 64 | public interface TapListener { 65 | void onBluetoothTurnedOn(); 66 | void onBluetoothTurnedOff(); 67 | void onTapStartConnecting(@NonNull String tapIdentifier); 68 | void onTapConnected(@NonNull String tapIdentifier); 69 | void onTapDisconnected(@NonNull String tapIdentifier); 70 | void onTapResumed(@NonNull String tapIdentifier); 71 | void onTapChanged(@NonNull String tapIdentifier); 72 | void onTapInputReceived(@NonNull String tapIdentifier, int data, int repeatData); 73 | void onTapShiftSwitchReceived(@NonNull String tapIdentifier, int data); 74 | void onMouseInputReceived(@NonNull String tapIdentifier, @NonNull MousePacket data); 75 | void onAirMouseInputReceived(@NonNull String tapIdentifier, @NonNull AirMousePacket data); 76 | void onRawSensorInputReceived(@NonNull String tapIdentifier, @NonNull RawSensorData rsData); 77 | void onTapChangedState(@NonNull String tapIdentifier, @NonNull int state); 78 | void onError(@NonNull String tapIdentifier, int code, @NonNull String description); 79 | } 80 | ``` 81 | Just implement it and pass it to `TapSdk` class by calling `sdk.registerTapListener(tapListener)`. 82 | 83 | ___ 84 | #### Important Note: 85 | `onTapInputReceived` is a callback function which will be triggered every time a specific TAP device was being tapped, and which fingers were tapped. The returned data is an integer representing a 8-bit unsigned number, between 1 and 31. It's binary form represents the fingers that are tapped. The LSB is thumb finger, the MSB (bit number 5) is the pinky finger. For example: if combination equals 3 - it's binary form is 10100, Which means that the thumb and the middle fingers were tapped. For your convenience, you can convert the binary format into fingers boolean array by calling static function `TapSdk.toFingers(tapInput)` listed below.. The repeat data parameter is one for single taps, 2 for double 3 for triple 86 | `onTapShiftSwitchReceived` is a callback function which will be triggered every time a specific TAP device was being tapped, holds the state of Shift (0=off, 1=on, 2=locked) and Switch (0=off, 1=on). The returned data is an integer representing a 8-bit unsigned number, between 1 and 31. It's binary form represents the fingers that are tapped. The LSB is thumb finger, the MSB (bit number 5) is the pinky finger. For example: if combination equals 3 - it's binary form is 10100, Which means that the thumb and the middle fingers were tapped. For your convenience, you can convert the binary format into an array of 2 ints by calling static function `TapSdk.toShiftAndSwitch(tapShiftSwitch)` listed below. 87 | ___ 88 | 89 | Tap Class 90 | ========= 91 | Using `Tap` class you can get basic information of your TAP device. 92 | A `Tap` instance can be generated only from connected, cached TAP devices, by simply calling `sdk.getCachedTap(tapIdentifier)`. 93 | 94 | Debugging 95 | ========= 96 | It is often desirable and useful to print out more `TapSdk` inner logs in LogCat. You can manually enable inner log prints by calling `sdk.enableDebug()`, and corresponding, you can disable inner log prints by calling `sdk.disableDebug()` 97 | 98 | TapSdk API 99 | ========== 100 | #### `void resume()` & `void pause()` 101 | > As mentioned, to correctly switch between Modes, `TapSdk` needs to be aware of your application's lifecycle, in particular when your application goes to the background and return from it, so it is needed for you to call the corresponding methods when such events occur. 102 |   103 |   104 | #### `void clearCacheOnTapDisconnection(boolean clearCacheOnTapDisconnection)` 105 | > When TAP device is connecting, `TapSdk` will cache some of it's data for quicker reconnection. You can change the default `TapSdk`'s behaviour by calling this method with the desired, new configuration. 106 |   107 |   108 | #### `void enableAutoSetControllerModeOnConnection() & void disableAutoSetControllerModeOnConnection()` 109 | #### ** DEPRECATED ** 110 | > Use SetDefaultMode instead: 111 | ```java 112 | public void setDefaultMode(TapInputMode mode, Boolean applyImmediate) 113 | ``` 114 | This will set the default mode for new connected devices. 115 | pass true to applyImmediate if you wish to apply this mode to current connected devices. 116 |   117 |   118 | #### `void enablePauseResumeHandling() & void disablePauseResumeHandling()` 119 | > One of `TapSdk`'s key features is the background handling. By calling `disablePauseResumeHandling` method you can disable this functionality, so going to background will not effect any TAP device, and it'll remain in the same Mode as it was before going to background.. 120 |   121 |   122 | #### `Set getConnectedTaps()` 123 | > If you wish at any point in your application, you can retrieve a set of connected TAPs. 124 |   125 |   126 | #### `void registerTapListener(TapListener listener)` 127 | > Pass `TapListener` to get all `TapSdk` callbacks. 128 |   129 |   130 | #### `void unregisterTapListener(TapListener listener)` 131 | > Unregister registered `TapListener`. 132 |   133 |   134 | #### `void startMode(String tapIdentifier, int mode)` 135 | #### ** DEPRECATED ** 136 | Use the following methods to change TAP mode: 137 | ```java 138 | public void startControllerMode(@NonNull String tapIdentifier); 139 | public void startTextMode(@NonNull String tapIdentifier); 140 | public void startControllerWithMouseHIDMode(@NonNull String tapIdentifier); 141 | public void startRawSensorMode(@NonNull String tapIdentifier, byte deviceAccelerometerSensitivity, byte imuGyroSensitivity, byte imuAccelerometerSensitivity); 142 | public void startControllerWithFullHIDMode(@NonNull String tapIdentifier); 143 | ``` 144 |   145 |   146 | #### `int getMode(String tapIdentifier)` 147 | #### ** DEPRECATED ** 148 |   149 |   150 | #### `boolean isInMode(String tapIdentifier, int mode)` 151 | #### ** DEPRECATED ** 152 | > Another helper method to check what Mode is enabled for a specific TAP device. 153 |   154 |   155 | #### `public void vibrate(@NonNull String tapIdentifier, int[] durations)` 156 | Send haptic/vibrations to a TAP device. 157 | durations: An array of durations in the format of haptic, pause, haptic, pause ... You can specify up to 18 elements in this array. The rest will be ignored. 158 | Each array element is defined in milliseconds. 159 | When [tapIdentifiers] is null or missing - the mode will be applied to ALL connected TAPs. 160 | Example: 161 | ```java 162 | sdk.vibrate(tapIdentifier, new int[] { 500,100,500}); 163 | ``` 164 | Will send two 500 milliseconds haptics with a 100 milliseconds pause in the middle. 165 |   166 |   167 | #### `void writeName(String tapIdentifier, String name)` 168 | > Write TAP device's name. 169 |   170 |   171 | #### `void close()` 172 | > Releasing associated inner bluetooth manager. 173 |   174 |   175 | #### `static boolean[] toFingers(int tapInput)` 176 | > As said before, the `tapInput` is an unsigned 8-bit integer. to convert it to array of booleans: 177 | ```Java 178 | boolean[] fingers = TapSdk.toFingers(tapInput); 179 | ``` 180 | > While: 181 | fingers\[0\] indicates if the thumb was tapped. 182 | fingers\[1\] indicates if the index finger was tapped. 183 | fingers\[2\] indicates if the middle finger was tapped. 184 | fingers\[3\] indicates if the ring finger was tapped. 185 | fingers\[4\] indicates if the pinky finger was tapped. 186 |   187 |   188 | 189 | #### `static boolean[] toShiftAndSwitch(int tapShiftSwitch)` 190 | > As said before, the `tapInput` is an unsigned 8-bit integer. to convert it to array of booleans: 191 | ```Java 192 | int[] shiftSwitch = TapSdk.toShiftAndSwitch(tapShiftSwitch); 193 | ``` 194 | > While: 195 | shiftSwitch\[0\] indicates the shift value (0=off, 1=on, 2=locked). 196 | shiftSwitch\[1\] indicates the switch value (0=off, !0=on). 197 |   198 |   199 | 200 | # Raw Sensor Mode 201 | In raw sensors mode, the TAP continuously sends raw data from the following sensors: 202 | 1. Five 3-axis accelerometers on each finger ring. 203 | 2. IMU (3-axis accelerometer + gyro) located on the thumb (**for TAP Strap 2 only**). 204 | 205 | ### To put a TAP into Raw Sensor Mode 206 | ```java 207 | public void startRawSensorMode(@NonNull String tapIdentifier, byte deviceAccelerometerSensitivity, byte imuGyroSensitivity, byte imuAccelerometerSensitivity); 208 | 209 | ... 210 | 211 | sdk.startRawSensorMode(tapIdentifier, (byte)0,(byte)0,(byte)0); 212 | ``` 213 | When puting TAP in Raw Sensor Mode, the sensitivities of the values can be defined by the developer. 214 | deviceAccelerometer refers to the sensitivities of the fingers' accelerometers. Range: 1 to 4. 215 | imuGyro refers to the gyro sensitivity on the thumb's sensor. Range: 1 to 4. 216 | imuAccelerometer refers to the accelerometer sensitivity on the thumb's sensor. Range: 1 to 5. 217 | The default value for all sensitivities is 0. 218 | 219 | ### Stream callback: 220 | 221 | ```java 222 | public void onRawSensorInputReceived(@NonNull String tapIdentifier,@NonNull RawSensorData rsData) { 223 | //RawSensorData Object has a timestamp, dataType and an array points(x,y,z). 224 | if (rsData .dataType == RawSensorData.DataType.Device) { 225 | // Fingers accelerometer. 226 | // Each point in array represents the accelerometer value of a finger (thumb, index, middle, ring, pinky). 227 | Point3 thumb = rsData.getPoint(RawSensorData.iDEV_INDEX); 228 | if (thumb != null) { 229 | double x = thumb.x; 230 | double y = thumb.y; 231 | double z = thumb.z; 232 | } 233 | // Etc... use indexes: RawSensorData.iDEV_THUMB, RawSensorData.iDEV_INDEX, RawSensorData.iDEV_MIDDLE, RawSensorData.iDEV_RING, RawSensorData.iDEV_PINKY 234 | } else if (data.dataType == RawSensorData.DataType.IMU) { 235 | // Refers to an additional accelerometer on the Thumb sensor and a Gyro (placed on the thumb unit as well). 236 | Point3 gyro = rsData.getPoint(RawSensorData.iIMU_GYRO); 237 | if (point3 != null) { 238 | double x = gyro.x; 239 | double y = gyro.y; 240 | double z = gyro.z; 241 | } 242 | // Etc... use indexes: RawSensorData.iIMU_GYRO, RawSensorData.iIMU_ACCELEROMETER 243 | } 244 | } 245 | ``` 246 | 247 | [For more information about raw sensor mode click here](https://tapwithus.atlassian.net/wiki/spaces/TD/pages/792002574/Tap+Strap+Raw+Sensors+Mode) 248 | 249 | # TAPXR Gestures (July 2024) - Spatial Control 250 | 251 | Added support to read the hand state while in AirMouse mode, for the TapXR device. 252 | 253 | Authorized developers can gain access to the experimantal Spatial Control features: 254 | 1. Extended AirGesture state - enabling aggregation for pinch, drag and swipe gestures. 255 | 2. Select input type - enabling the selection of input type to be activated - i.e. AirMouse/Tapping. 256 | 257 | These featureas are only available on TapXR and only for qualified developers. Request access [here](https://www.tapwithus.com/contact-us/) 258 | 259 | ## AirMousePacket 260 | 261 | Added 3 states for the class AirMousePacket: 262 | 263 | ```java 264 | public class AirMousePacket extends Packet { 265 | . 266 | . 267 | . 268 | public static final int XR_AIR_GESTURE_NONE = 100; 269 | public static final int XR_AIR_GESTURE_THUMB_INDEX = 101; 270 | public static final int XR_AIR_GESTURE_THUMB_MIDDLE = 102; 271 | ``` 272 | 273 | XR_AIR_GESTURE_NONE: The hand is in resting state. 274 | XR_AIR_GESTURE_THUMB_INDEX : the thumb is touching the index finger. 275 | XR_AIR_GESTURE_THUMB_MIDDLE : the thumb is touching the middle finger. 276 | 277 | These states will be sent continously multiple times per second. 278 | 279 | The best practice is the take the most common one out of the last 3 events received to allow margin for errors. 280 | 281 | This will allow you to combine these states and the mouse-move event into "Drag and Drop" Gesture for example. 282 | 283 | ##TapXRState 284 | 285 | In addition to TAPInputMode, the new TAPXR has input states. 286 | 287 | You can force TAPXR to switch to input state as follows: 288 | 289 | AIRMOUSE - The TAPXR will operate in AIRMOUSE mode ONLY. 290 | TAPPING - The TAPXR will operate in TAPPING mode only. 291 | USERCONTROL - The user will freely switch states as wished. 292 | 293 | 294 | Examples: 295 | 296 | ```java 297 | tapSdk.setDefaultXRState(TapXRState.tapping(), true); 298 | tapSdk.setDefaultXRState(TapXRState.userControl(), false); 299 | tapSdk.startXRAirMouseState("identifier"); 300 | ``` 301 | You can change the state of individual connected device or devices by calling one of these methods: 302 | 303 | ```java 304 | public void startXRUserControlState(@NonNull String tapIdentifier) 305 | public void startXRAirMouseState(@NonNull String tapIdentifier) 306 | public void startXRTappingState(@NonNull String tapIdentifier) 307 | ``` 308 | 309 | or call this function to set the default XR State: 310 | 311 | ```java 312 | public void setDefaultXRState(TapXRState state, Boolean applyImmediate) 313 | ``` 314 | 315 | While calling setDefaultTAPXRState, it'll set the default state that will be applied to newly connected devices. 316 | If you wish to apply this state to already-connected devices, call with "applyImmediate": true. 317 | 318 | 319 | Example app 320 | =========== 321 | The Android Studio project contains an example app where you can see how to use some of the features of `TapSdk`. 322 | 323 | Support 324 | =========== 325 | Please refer to the issues tab. 326 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 33 5 | defaultConfig { 6 | applicationId "com.tapwithus.tapsdk" 7 | minSdkVersion 23 8 | targetSdkVersion 33 9 | versionCode 2 10 | versionName "0.2.2" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | namespace 'com.tapwithus.tapsdk' 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(include: ['*.jar'], dir: 'libs') 24 | implementation 'androidx.appcompat:appcompat:1.5.1' 25 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 26 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 27 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 28 | testImplementation 'junit:junit:4.13.2' 29 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 30 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 31 | implementation project(':tap-android-sdk') 32 | // implementation 'com.tapwithus:tap-android-sdk:0.3.5' 33 | // implementation 'io.github.tapwithus:tap-android-sdk:0.3.5' 34 | } 35 | 36 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tapwithus/tapsdk/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.tapwithus.tapsdk; 2 | 3 | import android.content.Context; 4 | //import android.support.test.InstrumentationRegistry; 5 | //import android.support.test.runner.AndroidJUnit4; 6 | // 7 | //import org.junit.Test; 8 | //import org.junit.runner.RunWith; 9 | // 10 | //import static org.junit.Assert.*; 11 | // 12 | ///** 13 | // * Instrumented test, which will execute on an Android device. 14 | // * 15 | // * @see Testing documentation 16 | // */ 17 | //@RunWith(AndroidJUnit4.class) 18 | //public class ExampleInstrumentedTest { 19 | // @Test 20 | // public void useAppContext() throws Exception { 21 | // // Context of the app under test. 22 | // Context appContext = InstrumentationRegistry.getTargetContext(); 23 | // 24 | // assertEquals("com.tapwithus.tapsdk", appContext.getPackageName()); 25 | // } 26 | //} 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 12 | 13 | 14 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/tapwithus/tapsdk/RecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.tapwithus.tapsdk; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.constraintlayout.widget.ConstraintLayout; 5 | import androidx.recyclerview.widget.RecyclerView; 6 | import android.util.Log; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import com.tapwithus.sdk.TapSdk; 13 | 14 | import java.util.List; 15 | 16 | public class RecyclerViewAdapter extends RecyclerView.Adapter { 17 | 18 | private List dataSet; 19 | 20 | private boolean onBind; 21 | 22 | public RecyclerViewAdapter(List dataSet) { 23 | this.dataSet = dataSet; 24 | } 25 | 26 | public void updateList(List items) { 27 | if (!onBind) { 28 | dataSet.clear(); 29 | dataSet.addAll(items); 30 | notifyDataSetChanged(); 31 | } 32 | } 33 | 34 | @NonNull 35 | @Override 36 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 37 | 38 | ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(parent.getContext()) 39 | .inflate(R.layout.list_row, parent, false); 40 | 41 | return new ViewHolder(v); 42 | } 43 | 44 | @Override 45 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 46 | onBind = true; 47 | try { 48 | holder.bindTapListItem(dataSet.get(position)); 49 | } catch (IndexOutOfBoundsException e) { 50 | Log.e("RecyclerViewAdapter", "Mmm... " + e.getMessage()); 51 | } finally { 52 | onBind = false; 53 | } 54 | } 55 | 56 | @Override 57 | public int getItemCount() { 58 | return dataSet.size(); 59 | } 60 | 61 | public void addItem(TapListItem item) { 62 | for (TapListItem i : dataSet) { 63 | if (i.tapIdentifier.equals(item.tapIdentifier)) { 64 | return; 65 | } 66 | } 67 | if (!onBind) { 68 | dataSet.add(item); 69 | notifyItemInserted(dataSet.size()); 70 | } 71 | } 72 | 73 | public void removeItem(String tapIdentifier) { 74 | int position; 75 | for (position = 0; position < dataSet.size(); position++) { 76 | if (dataSet.get(position).tapIdentifier.equals(tapIdentifier)) { 77 | if (!onBind) { 78 | dataSet.remove(position); 79 | notifyItemRemoved(position); 80 | } 81 | break; 82 | } 83 | } 84 | } 85 | 86 | public void updateTapInput(String tapIdentifier, int tapInputInt, int repeatDataInt) { 87 | if (tapInputInt == 0) { 88 | return; 89 | } 90 | 91 | for (int position = 0; position < dataSet.size(); position++) { 92 | TapListItem item = dataSet.get(position); 93 | if (item.tapIdentifier.equals(tapIdentifier)) { 94 | if (!onBind) { 95 | item.tapInputInt = tapInputInt; 96 | item.tapRepeatInt = repeatDataInt; 97 | item.tapInputFingers = TapSdk.toFingers(tapInputInt); 98 | notifyItemChanged(position); 99 | } 100 | } 101 | } 102 | } 103 | 104 | public void updateTapSwitchShift(String tapIdentifier, int tapSwitchShiftInt) { 105 | // I think even a value of zero is meaningful 106 | 107 | for (int position = 0; position < dataSet.size(); position++) { 108 | TapListItem item = dataSet.get(position); 109 | if (item.tapIdentifier.equals(tapIdentifier)) { 110 | if (!onBind) { 111 | item.tapShiftSwitchInt = tapSwitchShiftInt; 112 | item.tapShiftAndSwitch = TapSdk.toShiftAndSwitch(tapSwitchShiftInt); 113 | notifyItemChanged(position); 114 | } 115 | } 116 | } 117 | } 118 | 119 | public void updateName(String tapIdentifier, String name) { 120 | for (int position = 0; position < dataSet.size(); position++) { 121 | TapListItem item = dataSet.get(position); 122 | if (item.tapIdentifier.equals(tapIdentifier)) { 123 | if (!onBind) { 124 | item.tapName = name; 125 | notifyItemChanged(position); 126 | } 127 | break; 128 | } 129 | } 130 | } 131 | 132 | public void updateFwVer(String tapIdentifier, String fwVer) { 133 | for (int position = 0; position < dataSet.size(); position++) { 134 | TapListItem item = dataSet.get(position); 135 | if (item.tapIdentifier.equals(tapIdentifier)) { 136 | if (!onBind) { 137 | item.tapFwVer = fwVer; 138 | notifyItemChanged(position); 139 | } 140 | break; 141 | } 142 | } 143 | } 144 | 145 | public void onTextModeStarted(String tapIdentifier) { 146 | changeMode(tapIdentifier, false); } 147 | 148 | public void onControllerModeStarted(String tapIdentifier) { 149 | changeMode(tapIdentifier, true); 150 | } 151 | 152 | private void changeMode(String tapIdentifier, boolean isInControllerMode) { 153 | for (int position = 0; position < dataSet.size(); position++) { 154 | TapListItem item = dataSet.get(position); 155 | if (item.tapIdentifier.equals(tapIdentifier)) { 156 | if (!onBind) { 157 | item.isInControllerMode = isInControllerMode; 158 | notifyItemChanged(position); 159 | } 160 | break; 161 | } 162 | } 163 | } 164 | 165 | public static class ViewHolder extends RecyclerView.ViewHolder { 166 | 167 | public ConstraintLayout itemView; 168 | public TextView tapName; 169 | public TextView tapIdentifier; 170 | public TextView tapInputInt; 171 | public View finger1; 172 | public View finger2; 173 | public View finger3; 174 | public View finger4; 175 | public View finger5; 176 | public TextView mode; 177 | public TextView fwVer; 178 | public TextView shiftState; 179 | public TextView switchState; 180 | public TextView specialChar; 181 | 182 | public ViewHolder(ConstraintLayout itemView) { 183 | super(itemView); 184 | this.itemView = itemView; 185 | 186 | itemView.setOnClickListener(new View.OnClickListener() { 187 | @Override 188 | public void onClick(View v) { 189 | 190 | } 191 | }); 192 | 193 | tapName = itemView.findViewById(R.id.tapName); 194 | tapIdentifier = itemView.findViewById(R.id.tapAddress); 195 | tapInputInt = itemView.findViewById(R.id.tapInputInt); 196 | finger1 = itemView.findViewById(R.id.finger1); 197 | finger2 = itemView.findViewById(R.id.finger2); 198 | finger3 = itemView.findViewById(R.id.finger3); 199 | finger4 = itemView.findViewById(R.id.finger4); 200 | finger5 = itemView.findViewById(R.id.finger5); 201 | mode = itemView.findViewById(R.id.tapMode); 202 | fwVer = itemView.findViewById(R.id.tapFwVer); 203 | shiftState = itemView.findViewById(R.id.shiftState); 204 | switchState = itemView.findViewById(R.id.switchState); 205 | specialChar = itemView.findViewById(R.id.specialChar); 206 | } 207 | 208 | public void bindTapListItem(final TapListItem listItem) { 209 | itemView.setOnClickListener(new View.OnClickListener() { 210 | @Override 211 | public void onClick(View v) { 212 | listItem.onClickListener.onClick(listItem); 213 | } 214 | }); 215 | tapName.setText(listItem.tapName); 216 | tapIdentifier.setText(listItem.tapIdentifier); 217 | tapInputInt.setText(String.valueOf(listItem.tapInputInt)); 218 | if (listItem.tapInputFingers != null) { 219 | finger1.setBackgroundResource(listItem.tapInputFingers[0] ? R.drawable.circle_filled : R.drawable.circle_empty); 220 | finger2.setBackgroundResource(listItem.tapInputFingers[1] ? R.drawable.circle_filled : R.drawable.circle_empty); 221 | finger3.setBackgroundResource(listItem.tapInputFingers[2] ? R.drawable.circle_filled : R.drawable.circle_empty); 222 | finger4.setBackgroundResource(listItem.tapInputFingers[3] ? R.drawable.circle_filled : R.drawable.circle_empty); 223 | finger5.setBackgroundResource(listItem.tapInputFingers[4] ? R.drawable.circle_filled : R.drawable.circle_empty); 224 | } 225 | if (listItem.tapShiftAndSwitch != null) { 226 | switch (listItem.tapShiftAndSwitch[0]) { 227 | case 0: 228 | shiftState.setText("Shift OFF"); 229 | break; 230 | case 1: 231 | shiftState.setText("Shift ON"); 232 | break; 233 | case 2: 234 | shiftState.setText("Shift LOCK"); 235 | break; 236 | default: 237 | shiftState.setText("Shift ERROR!!!"); 238 | } 239 | if (listItem.tapShiftAndSwitch[1] > 0) { 240 | switchState.setText("Switch ON"); 241 | } else { 242 | switchState.setText("Switch OFF"); 243 | } 244 | } 245 | mode.setText(listItem.isInControllerMode ? "Controller Mode" : "Text Mode"); 246 | fwVer.setText(listItem.tapFwVer); 247 | specialChar.setText("Repeat = " + listItem.tapRepeatInt); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /app/src/main/java/com/tapwithus/tapsdk/TapListItem.java: -------------------------------------------------------------------------------- 1 | package com.tapwithus.tapsdk; 2 | 3 | public class TapListItem { 4 | 5 | public String tapIdentifier; 6 | public String tapName; 7 | public String tapFwVer; 8 | public int tapInputInt; 9 | public int tapShiftSwitchInt; 10 | public int tapRepeatInt; 11 | public boolean[] tapInputFingers; 12 | public int[] tapShiftAndSwitch; 13 | public boolean isInControllerMode = true; 14 | 15 | public TapListItemOnClickListener onClickListener; 16 | 17 | TapListItem(String tapIdentifier, TapListItemOnClickListener listener) { 18 | this.tapIdentifier = tapIdentifier; 19 | this.onClickListener = listener; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/tapwithus/tapsdk/TapListItemOnClickListener.java: -------------------------------------------------------------------------------- 1 | package com.tapwithus.tapsdk; 2 | 3 | public interface TapListItemOnClickListener { 4 | void onClick(TapListItem item); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/tapwithus/tapsdk/TestPacket.java: -------------------------------------------------------------------------------- 1 | package com.tapwithus.tapsdk; 2 | 3 | import com.tapwithus.sdk.bluetooth.Packet; 4 | 5 | public class TestPacket extends Packet { 6 | 7 | public TestPacket(byte[] data) { 8 | super(data, 8); 9 | } 10 | 11 | // public PacketValue dx = new PacketValue(0, 15); 12 | // public PacketValue dy = new PacketValue(16, 31); 13 | // public PacketValue dt = new PacketValue(32, 63); 14 | 15 | public PacketValue string4B = new PacketValue(0, 64); 16 | // public PacketValue int2B= new PacketValue(32, 16); 17 | // public PacketValue int4B= new PacketValue(48, 32); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_filled.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |