├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── androidthings │ │ └── robocar │ │ ├── BoardDefaults.java │ │ ├── CarController.java │ │ ├── CompanionConnection.java │ │ ├── RobocarActivity.java │ │ ├── RobocarAdvertiser.java │ │ ├── RobocarViewModel.java │ │ └── TricolorLed.java │ └── res │ └── values │ └── strings.xml ├── build.gradle ├── companion ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── androidthings │ │ └── robocar │ │ └── companion │ │ ├── CompanionActivity.java │ │ ├── CompanionViewModel.java │ │ ├── ControllerFragment.java │ │ ├── RobocarConnection.java │ │ ├── RobocarConnectionDialog.java │ │ ├── RobocarDiscoverer.java │ │ ├── RobocarDiscoveryFragment.java │ │ ├── RobocarEndpoint.java │ │ └── RobocarEndpointsAdapter.java │ └── res │ ├── animator │ └── button_state_list_anim_material.xml │ ├── color │ └── car_control_button.xml │ ├── drawable │ ├── ic_close_24dp.xml │ ├── ic_down_black_48dp.xml │ ├── ic_stop_black_48dp.xml │ ├── ic_turn_left_black_48dp.xml │ ├── ic_turn_right_black_48dp.xml │ └── ic_up_black_48dp.xml │ ├── layout-land │ └── fragment_controller.xml │ ├── layout │ ├── activity_companion.xml │ ├── fragment_auth_dialog.xml │ ├── fragment_controller.xml │ ├── fragment_discoverer.xml │ ├── fragment_robocar_discovery.xml │ └── list_item_robocar_endpoint.xml │ ├── menu │ └── controller.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 │ ├── dimens.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── shared ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── example │ └── androidthings │ └── robocar │ └── shared │ ├── CarCommands.java │ ├── ConnectorFragment.java │ ├── NearbyConnection.java │ ├── NearbyConnectionManager.java │ ├── PreferenceUtils.java │ ├── lifecycle │ └── ConfigResistantObserver.java │ └── model │ ├── AdvertisingInfo.java │ └── DiscovererInfo.java └── res └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | /.idea 11 | app/assets 12 | Icon? 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android Things Robocar 2 | ============ 3 | 4 | Introduction 5 | ------------ 6 | This project contains all the code required to build a robot car that runs on 7 | [Android Things](https://developer.android.com/things/index.html), as well as a companion "controller" Android app. 8 | 9 | This sample uses the following Google platforms and APIs: 10 | 11 | - [Android Things](https://developer.android.com/things/index.html) - The car's onboard operating system. 12 | - [Nearby APIs](https://developers.google.com/nearby/) - Local communication API used 13 | for pairing the robocar to a companion app which controls the car. 14 | 15 | > **Note:** The Android Things Console will be turned down for non-commercial 16 | > use on January 5, 2022. For more details, see the 17 | > [FAQ page](https://developer.android.com/things/faq). 18 | 19 | Pre-requisites 20 | -------------- 21 | To build the car, you will need the following hardware: 22 | 23 | - An [Android-Things powered device](https://developer.android.com/things/hardware/developer-kits.html) with a connected camera. This demo ran on a Raspberry Pi 3, but is in no way limited to that device. 24 | 25 | - A robot car chassis. We had great luck with the [Runt rover robotics kit](https://www.amazon.com/Actobotics-Junior-Runt-Rover/dp/B00UAWVC64). 26 | 27 | - A power source. The developer board and DC motors each need their own power input, so you can either get two small power packs or something like the [Anker Powercore 13000](https://smile.amazon.com/Anker-PowerCore-13000-Portable-Charger/dp/B00Z9QVE4Q/), which has two USB-out ports. 28 | 29 | - The [Adafruit stepper & DC Motor hat](https://www.adafruit.com/product/2348), for controlling the motors. 30 | 31 | (Optional) 32 | To assist in distinguishing between multiple robocars during the pairing process, the robocar also supports alphanumeric displays or RGB LED's for displaying a pairing code. Note that this is meant to _assist_ the user, 33 | and is not intended or sufficient as a security measure. If you'd like to add these to your setup, you'll need: 34 | 35 | - [A quad alphanumeric display](https://www.adafruit.com/product/1912) 36 | 37 | - [An RGB Led](https://www.adafruit.com/product/159) 38 | 39 | - [A half-size breadboard](https://www.adafruit.com/product/64) 40 | 41 | 42 | Support and Discussion 43 | ------- 44 | 45 | - Google+ IoT Developer Community Community: https://plus.google.com/communities/107507328426910012281 46 | - Stack Overflow: https://stackoverflow.com/tags/android-things/ 47 | 48 | If you've found an error in this sample, please file an issue: 49 | https://github.com/androidthings/robocar/issues 50 | 51 | Patches are encouraged, and may be submitted by forking this project and 52 | submitting a pull request through GitHub. 53 | 54 | Enable auto-launch behavior 55 | --------------------------- 56 | 57 | This Android Things app is currently configured to launch only when deployed from your 58 | development machine. To enable the main activity to launch automatically on boot, 59 | add the following `intent-filter` to the app's manifest file: 60 | 61 | ```xml 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ``` 72 | 73 | License 74 | ------- 75 | 76 | Copyright 2017 Google, Inc. 77 | 78 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 79 | license agreements. See the NOTICE file distributed with this work for 80 | additional information regarding copyright ownership. The ASF licenses this 81 | file to you under the Apache License, Version 2.0 (the "License"); you may not 82 | use this file except in compliance with the License. You may obtain a copy of 83 | the License at 84 | 85 | http://www.apache.org/licenses/LICENSE-2.0 86 | 87 | Unless required by applicable law or agreed to in writing, software 88 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 89 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 90 | License for the specific language governing permissions and limitations under 91 | the License. 92 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | android { 20 | compileSdkVersion 27 21 | defaultConfig { 22 | applicationId "com.example.androidthings.robocar" 23 | minSdkVersion 27 24 | targetSdkVersion 27 25 | versionCode 1 26 | versionName "1.0" 27 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | } 36 | 37 | configurations.all { 38 | resolutionStrategy { 39 | // resolve version conflict with play-services 40 | force 'com.android.support:support-fragment:26.1.0' 41 | } 42 | } 43 | 44 | dependencies { 45 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 46 | exclude group: 'com.android.support', module: 'support-annotations' 47 | }) 48 | testImplementation 'junit:junit:4.12' 49 | 50 | compileOnly 'com.google.android.things:androidthings:1.0' 51 | 52 | implementation 'com.google.android.gms:play-services:11.6.2' 53 | implementation project(path: ':shared') 54 | 55 | implementation 'com.google.android.things.contrib:driver-motorhat:1.0' 56 | implementation 'com.google.android.things.contrib:driver-ht16k33:1.0' 57 | implementation 'com.google.android.things.contrib:driver-button:1.0' 58 | } 59 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/google/home/alexlucas/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/BoardDefaults.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import android.os.Build; 19 | 20 | class BoardDefaults { 21 | private static final String DEVICE_RPI3 = "rpi3"; 22 | private static final String DEVICE_RPI3BP = "rpi3bp"; 23 | private static final String DEVICE_IMX7D_PICO = "imx7d_pico"; 24 | 25 | public static String getI2cBus() { 26 | switch (Build.DEVICE) { 27 | case DEVICE_RPI3: 28 | case DEVICE_RPI3BP: 29 | return "I2C1"; 30 | case DEVICE_IMX7D_PICO: 31 | return "I2C1"; 32 | default: 33 | throw new IllegalArgumentException("Unknown device: " + Build.DEVICE); 34 | } 35 | } 36 | 37 | public static String[] getLedGpioPins() { 38 | switch (Build.DEVICE) { 39 | case DEVICE_RPI3: 40 | case DEVICE_RPI3BP: 41 | return new String[]{"BCM5", "BCM6", "BCM12"}; 42 | case DEVICE_IMX7D_PICO: 43 | return new String[]{"GPIO2_IO02", "GPIO2_IO07", "GPIO2_IO00"}; 44 | default: 45 | throw new IllegalArgumentException("Unknown device: " + Build.DEVICE); 46 | } 47 | } 48 | 49 | public static String getButtonGpioPin() { 50 | switch (Build.DEVICE) { 51 | case DEVICE_RPI3: 52 | case DEVICE_RPI3BP: 53 | return "BCM26"; 54 | case DEVICE_IMX7D_PICO: 55 | return "GPIO2_IO01"; 56 | default: 57 | throw new IllegalArgumentException("Unknown device: " + Build.DEVICE); 58 | } 59 | } 60 | 61 | private BoardDefaults() { 62 | //no instance 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/CarController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import android.os.Handler; 19 | import android.os.HandlerThread; 20 | import android.text.TextUtils; 21 | import android.util.Log; 22 | 23 | import com.example.androidthings.robocar.TricolorLed.Tricolor; 24 | import com.example.androidthings.robocar.shared.CarCommands; 25 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo.LedColor; 26 | import com.google.android.things.contrib.driver.motorhat.MotorHat; 27 | import com.google.android.things.contrib.driver.ht16k33.AlphanumericDisplay; 28 | 29 | import java.io.IOException; 30 | import java.util.List; 31 | 32 | 33 | public class CarController { 34 | 35 | private static final String TAG = "CarController"; 36 | 37 | private static final int[] ALL_MOTORS = {0, 1, 2, 3}; 38 | private static final int[] LEFT_MOTORS = {0, 2}; 39 | private static final int[] RIGHT_MOTORS = {1, 3}; 40 | 41 | private static final int SPEED_NORMAL = 100; 42 | private static final int SPEED_TURNING_INSIDE = 70; 43 | private static final int SPEED_TURNING_OUTSIDE = 250; 44 | 45 | private MotorHat mMotorHat; 46 | 47 | private TricolorLed mLed; 48 | private LedPatternBlinker mBlinker; 49 | 50 | private AlphanumericDisplay mDisplay; 51 | private MarqueeRunnable mDisplayRunnable; 52 | 53 | private HandlerThread mHandlerThread; 54 | private Handler mHandler; 55 | 56 | public CarController(MotorHat motorHat, TricolorLed led, AlphanumericDisplay display) { 57 | mMotorHat = motorHat; 58 | mLed = led; 59 | mDisplay = display; 60 | 61 | mHandlerThread = new HandlerThread("CarController-worker"); 62 | mHandlerThread.start(); 63 | mHandler = new Handler(mHandlerThread.getLooper()); 64 | } 65 | 66 | public void shutDown() { 67 | stop(); 68 | clearBlinker(); 69 | mHandlerThread.quit(); 70 | } 71 | 72 | // Motor controls 73 | 74 | public boolean onCarCommand(int command) { 75 | switch (command) { 76 | case CarCommands.GO_FORWARD: 77 | return goForward(); 78 | case CarCommands.GO_BACK: 79 | return goBackward(); 80 | case CarCommands.STOP: 81 | return stop(); 82 | case CarCommands.TURN_LEFT: 83 | return turnLeft(); 84 | case CarCommands.TURN_RIGHT: 85 | return turnRight(); 86 | } 87 | return false; 88 | } 89 | 90 | private boolean goForward() { 91 | return setSpeed(SPEED_NORMAL) && setMotorState(MotorHat.MOTOR_STATE_CW, ALL_MOTORS); 92 | } 93 | 94 | private boolean goBackward() { 95 | return setSpeed(SPEED_NORMAL) && setMotorState(MotorHat.MOTOR_STATE_CCW, ALL_MOTORS); 96 | } 97 | 98 | private boolean stop() { 99 | return setMotorState(MotorHat.MOTOR_STATE_RELEASE, ALL_MOTORS); 100 | } 101 | 102 | private boolean turnLeft() { 103 | return turn(LEFT_MOTORS, RIGHT_MOTORS); 104 | } 105 | 106 | private boolean turnRight() { 107 | return turn(RIGHT_MOTORS, LEFT_MOTORS); 108 | } 109 | 110 | private boolean setMotorState(int state, int... motors) { 111 | try { 112 | if (motors != null && motors.length > 0) { 113 | for (int motor : motors) { 114 | mMotorHat.setMotorState(motor, state); 115 | } 116 | } 117 | return true; 118 | } catch (IOException e) { 119 | Log.e(TAG, "Error setting motor state", e); 120 | return false; 121 | } 122 | } 123 | 124 | private boolean turn(int[] insideMotors, int[] outsideMotors) { 125 | try { 126 | setMotorState(MotorHat.MOTOR_STATE_CW, ALL_MOTORS); 127 | 128 | for (int motor : insideMotors) { 129 | mMotorHat.setMotorSpeed(motor, SPEED_TURNING_INSIDE); 130 | } 131 | for (int motor : outsideMotors) { 132 | mMotorHat.setMotorSpeed(motor, SPEED_TURNING_OUTSIDE); 133 | } 134 | return true; 135 | } catch (IOException e) { 136 | Log.e(TAG, "Error setting motor state", e); 137 | return false; 138 | } 139 | } 140 | 141 | private boolean setSpeed(int speed) { 142 | try { 143 | for (int motor : ALL_MOTORS) { 144 | mMotorHat.setMotorSpeed(motor, speed); 145 | } 146 | return true; 147 | } catch (IOException e) { 148 | Log.e(TAG, "Error setting speed", e); 149 | return false; 150 | } 151 | } 152 | 153 | // LED controls 154 | 155 | public void setLedColor(final @Tricolor int color) { 156 | clearBlinker(); 157 | mHandler.post(new Runnable() { 158 | @Override 159 | public void run() { 160 | mLed.setColor(color); 161 | } 162 | }); 163 | } 164 | 165 | public void setLedSequence(List colors) { 166 | clearBlinker(); 167 | if (colors != null && !colors.isEmpty()) { 168 | final int size = colors.size() + 2; 169 | int[] pattern = new int[size]; 170 | for (int i = 0; i < size - 2; i++) { 171 | LedColor color = colors.get(i); 172 | pattern[i] = TricolorLed.ledColorToTricolor(color); 173 | } 174 | // Add 2 OFF beats 175 | pattern[size - 2] = pattern[size - 1] = TricolorLed.OFF; 176 | blinkLed(pattern, LedPatternBlinker.REPEAT_INFINITE); 177 | } 178 | } 179 | 180 | private void blinkLed(int[] colors, int repeatCount) { 181 | clearBlinker(); 182 | mBlinker = new LedPatternBlinker(colors, repeatCount); 183 | mHandler.post(mBlinker); 184 | } 185 | 186 | private void clearBlinker() { 187 | if (mBlinker != null) { 188 | mBlinker.mCanceled = true; // removeCallbacks() might not catch it in time. 189 | mHandler.removeCallbacks(mBlinker); 190 | mBlinker = null; 191 | } 192 | } 193 | 194 | public void display(String text) { 195 | if (mDisplay != null) { 196 | if (mDisplayRunnable != null && TextUtils.equals(text, mDisplayRunnable.mText)) { 197 | return; // Avoid restarting the marquee for the same text 198 | } 199 | clearDisplay(); 200 | mDisplayRunnable = new MarqueeRunnable(text); 201 | mHandler.post(mDisplayRunnable); 202 | } 203 | } 204 | 205 | private void clearDisplay() { 206 | if (mDisplayRunnable != null) { 207 | mDisplayRunnable.mCanceled = true; // removeCallbacks() might not catch it in time. 208 | mHandler.removeCallbacks(mDisplayRunnable); 209 | mDisplayRunnable = null; 210 | try { 211 | mDisplay.clear(); 212 | } catch (IOException e) { 213 | Log.d(TAG, "Error clearing display"); 214 | } 215 | } 216 | } 217 | 218 | private class LedPatternBlinker implements Runnable { 219 | 220 | static final long BLINK_MS = 400L; 221 | static final int REPEAT_INFINITE = -1; 222 | 223 | private final int[] mColors; 224 | private final int mRepeatCount; 225 | private boolean mCanceled; 226 | private int mCount = 0; 227 | private int mIndex = 0; 228 | 229 | LedPatternBlinker(int[] colors, int repeatCount) { 230 | mColors = colors; 231 | mRepeatCount = repeatCount; 232 | } 233 | 234 | @Override 235 | public void run() { 236 | if (mCanceled || mColors == null || mColors.length == 0) { 237 | return; // nothing to blink 238 | } 239 | if (mRepeatCount < 0 || mCount <= mRepeatCount) { 240 | mLed.setColor(mColors[mIndex++]); 241 | if (mIndex >= mColors.length) { 242 | mIndex = 0; 243 | mCount++; 244 | } 245 | 246 | mHandler.postDelayed(this, BLINK_MS); 247 | } 248 | } 249 | } 250 | 251 | private class MarqueeRunnable implements Runnable { 252 | 253 | static final long MARQUEE_DELAY_MS = 400L; 254 | 255 | private final String mText; 256 | private final int mTextSize; 257 | private final int mMarqueeSize; 258 | private int mIndex = 0; 259 | private boolean mCanceled; 260 | 261 | public MarqueeRunnable(String text) { 262 | mText = text; 263 | mTextSize = text == null ? 0 : text.length(); 264 | mMarqueeSize = mTextSize + 4; 265 | } 266 | 267 | @Override 268 | public void run() { 269 | if (mCanceled || mTextSize < 1) { 270 | return; 271 | } 272 | 273 | for (int i = 0; i < 4; i++) { 274 | int p = mIndex - i; 275 | char c = (p < 0 || p >= mTextSize) ? ' ' : mText.charAt(p); 276 | try { 277 | mDisplay.display(c, 3 - i, false); 278 | } catch (IOException e) { 279 | Log.d(TAG, "Error writing to display"); 280 | } 281 | } 282 | 283 | if (++mIndex > mMarqueeSize) { 284 | mIndex = 0; 285 | } 286 | 287 | mHandler.postDelayed(this, MARQUEE_DELAY_MS); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/CompanionConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import com.example.androidthings.robocar.shared.NearbyConnection; 19 | import com.example.androidthings.robocar.shared.model.DiscovererInfo; 20 | 21 | /** 22 | * Handle for a connection to a Companion 23 | */ 24 | public class CompanionConnection extends NearbyConnection { 25 | 26 | private final DiscovererInfo mDiscovererInfo; 27 | private final RobocarAdvertiser mRobocarAdvertiser; 28 | 29 | public CompanionConnection(String endpointId, DiscovererInfo discovererInfo, 30 | RobocarAdvertiser advertiser) { 31 | super(endpointId, advertiser); 32 | if (discovererInfo == null) { 33 | throw new IllegalArgumentException("DiscovererInfo cannot be null"); 34 | } 35 | mDiscovererInfo = discovererInfo; 36 | mRobocarAdvertiser = advertiser; 37 | } 38 | 39 | public DiscovererInfo getDiscovererInfo() { 40 | return mDiscovererInfo; 41 | } 42 | 43 | public boolean isAuthenticating() { 44 | return getState() == ConnectionState.AUTHENTICATING 45 | || getState() == ConnectionState.AUTH_ACCEPTED; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/RobocarActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.androidthings.robocar; 18 | 19 | import android.arch.lifecycle.Observer; 20 | import android.arch.lifecycle.ViewModelProviders; 21 | import android.content.SharedPreferences; 22 | import android.os.Bundle; 23 | import android.os.Handler; 24 | import android.preference.PreferenceManager; 25 | import android.support.annotation.Nullable; 26 | import android.support.v7.app.AppCompatActivity; 27 | import android.util.Log; 28 | import android.view.KeyEvent; 29 | 30 | import com.example.androidthings.robocar.shared.CarCommands; 31 | import com.example.androidthings.robocar.shared.ConnectorFragment; 32 | import com.example.androidthings.robocar.shared.ConnectorFragment.ConnectorCallbacks; 33 | import com.example.androidthings.robocar.shared.PreferenceUtils; 34 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 35 | import com.google.android.things.contrib.driver.motorhat.MotorHat; 36 | import com.google.android.gms.common.ConnectionResult; 37 | import com.google.android.gms.nearby.connection.Payload; 38 | import com.google.android.gms.nearby.connection.PayloadCallback; 39 | import com.google.android.gms.nearby.connection.PayloadTransferUpdate; 40 | import com.google.android.things.contrib.driver.button.Button.LogicState; 41 | import com.google.android.things.contrib.driver.button.ButtonInputDriver; 42 | import com.google.android.things.contrib.driver.ht16k33.AlphanumericDisplay; 43 | 44 | import java.io.IOException; 45 | 46 | 47 | public class RobocarActivity extends AppCompatActivity implements ConnectorCallbacks { 48 | 49 | private static final String TAG = "RobocarActivity"; 50 | private static final long DISCONNECT_DELAY = 2500L; //ms 51 | private static final long RESET_DELAY = 5000L; //ms 52 | 53 | private AdvertisingInfo mAdvertisingInfo; 54 | private RobocarAdvertiser mNearbyAdvertiser; 55 | private CompanionConnection mCompanionConnection; 56 | 57 | private MotorHat mMotorHat; 58 | private TricolorLed mLed; 59 | private AlphanumericDisplay mDisplay; 60 | private ButtonInputDriver mButtonInputDriver; 61 | 62 | private CarController mCarController; 63 | private RobocarViewModel mViewModel; 64 | 65 | private boolean mIsAdvertising; 66 | 67 | private Handler mResetHandler; 68 | private boolean mKeyPressed; 69 | 70 | PayloadCallback mPayloadListener = new PayloadCallback() { 71 | @Override 72 | public void onPayloadReceived(String s, Payload payload) { 73 | byte[] bytes = CarCommands.fromPayload(payload); 74 | if (bytes == null || bytes.length == 0) { 75 | return; 76 | } 77 | 78 | byte command = bytes[0]; 79 | Log.d(TAG, "onPayloadReceived: Command: " + command); 80 | byte response = CarCommands.ERROR; 81 | if (mCarController != null && mCarController.onCarCommand(command)) { 82 | response = command; 83 | } 84 | mCompanionConnection.sendCommand(response); 85 | if (response == CarCommands.ERROR) { 86 | // TODO flash red 87 | } 88 | } 89 | 90 | @Override 91 | public void onPayloadTransferUpdate(String s, PayloadTransferUpdate payloadTransferUpdate) { 92 | } 93 | }; 94 | 95 | @Override 96 | protected void onCreate(Bundle savedInstanceState) { 97 | super.onCreate(savedInstanceState); 98 | // init AdvertisingInfo 99 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 100 | mAdvertisingInfo = PreferenceUtils.loadAdvertisingInfo(prefs); 101 | if (mAdvertisingInfo == null) { 102 | mAdvertisingInfo = AdvertisingInfo.generateAdvertisingInfo(); 103 | PreferenceUtils.saveAdvertisingInfo(prefs, mAdvertisingInfo); 104 | } 105 | 106 | try { 107 | mMotorHat = new MotorHat(BoardDefaults.getI2cBus()); 108 | } catch (IOException e) { 109 | throw new RuntimeException("Failed to create MotorHat", e); 110 | } 111 | try { 112 | mDisplay = new AlphanumericDisplay(BoardDefaults.getI2cBus()); 113 | mDisplay.setEnabled(true); 114 | mDisplay.setBrightness(0.5f); 115 | mDisplay.clear(); 116 | } catch (IOException e) { 117 | // We may not have a display, which is OK. CarController only uses it if it's not null. 118 | Log.e(TAG, "Failed to open display.", e); 119 | mDisplay = null; 120 | } 121 | try { 122 | mButtonInputDriver = new ButtonInputDriver(BoardDefaults.getButtonGpioPin(), 123 | LogicState.PRESSED_WHEN_HIGH, KeyEvent.KEYCODE_A); 124 | mButtonInputDriver.register(); 125 | } catch (IOException e) { 126 | Log.e(TAG, "Failed to open button driver.", e); 127 | mButtonInputDriver = null; 128 | } 129 | 130 | String[] ledPins = BoardDefaults.getLedGpioPins(); 131 | mLed = new TricolorLed(ledPins[0], ledPins[1], ledPins[2]); 132 | mCarController = new CarController(mMotorHat, mLed, mDisplay); 133 | 134 | mResetHandler = new Handler(); 135 | 136 | mViewModel = ViewModelProviders.of(this).get(RobocarViewModel.class); 137 | mNearbyAdvertiser = mViewModel.getRobocarAdvertiser(); 138 | 139 | mNearbyAdvertiser.setAdvertisingInfo(mAdvertisingInfo); 140 | mNearbyAdvertiser.setPairedDiscovererInfo(PreferenceUtils.loadDiscovererInfo(prefs)); 141 | mNearbyAdvertiser.getAdvertisingLiveData().observe(this, new Observer() { 142 | @Override 143 | public void onChanged(@Nullable Boolean value) { 144 | mIsAdvertising = value == null ? false : value; 145 | updateUi(); 146 | } 147 | }); 148 | mNearbyAdvertiser.getCompanionConnectionLiveData().observe(this, 149 | new Observer() { 150 | @Override 151 | public void onChanged(@Nullable CompanionConnection connection) { 152 | setConnection(connection); 153 | } 154 | }); 155 | 156 | if (savedInstanceState == null) { 157 | // First launch. Attach the connector fragment and give it our client to connect. 158 | ConnectorFragment.attachTo(this, mViewModel.getGoogleApiClient()); 159 | } 160 | } 161 | 162 | @Override 163 | protected void onStart() { 164 | super.onStart(); 165 | mNearbyAdvertiser.setPayloadListener(mPayloadListener); 166 | } 167 | 168 | @Override 169 | protected void onStop() { 170 | super.onStop(); 171 | mNearbyAdvertiser.setPayloadListener(null); 172 | } 173 | 174 | @Override 175 | protected void onDestroy() { 176 | super.onDestroy(); 177 | if (mCarController != null) { 178 | mCarController.shutDown(); 179 | } 180 | 181 | if (mMotorHat != null) { 182 | try { 183 | mMotorHat.close(); 184 | } catch (IOException e) { 185 | Log.e(TAG, "Error closing MotorHat", e); 186 | } finally { 187 | mMotorHat = null; 188 | } 189 | } 190 | 191 | if (mLed != null) { 192 | try { 193 | mLed.setColor(TricolorLed.OFF); 194 | mLed.close(); 195 | } catch (Exception e) { 196 | Log.e(TAG, "Error closing LED", e); 197 | } finally { 198 | mLed = null; 199 | } 200 | } 201 | 202 | if (mDisplay != null) { 203 | try { 204 | mDisplay.clear(); 205 | mDisplay.setBrightness(0); 206 | mDisplay.setEnabled(false); 207 | mDisplay.close(); 208 | } catch (IOException e) { 209 | Log.e(TAG, "Error closing display", e); 210 | } finally { 211 | mDisplay = null; 212 | } 213 | } 214 | 215 | if (mButtonInputDriver != null) { 216 | mButtonInputDriver.unregister(); 217 | try { 218 | mButtonInputDriver.close(); 219 | } catch (IOException e) { 220 | Log.e(TAG, "Error closing button driver", e); 221 | } finally{ 222 | mButtonInputDriver = null; 223 | } 224 | } 225 | } 226 | 227 | @Override 228 | public boolean onKeyDown(int keyCode, KeyEvent event) { 229 | if (keyCode == KeyEvent.KEYCODE_A) { //29 230 | if (!mKeyPressed) { 231 | mKeyPressed = true; 232 | mResetHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY); 233 | mResetHandler.postDelayed(mResetRunnable, RESET_DELAY); 234 | 235 | } 236 | return true; 237 | } 238 | return super.onKeyDown(keyCode, event); 239 | } 240 | 241 | @Override 242 | public boolean onKeyUp(int keyCode, KeyEvent event) { 243 | if (keyCode == KeyEvent.KEYCODE_A) { //29 244 | mKeyPressed = false; 245 | // No effect if these have already run 246 | mResetHandler.removeCallbacks(mDisconnectRunnable); 247 | mResetHandler.removeCallbacks(mResetRunnable); 248 | return true; 249 | } 250 | return handleKeyCode(keyCode) || super.onKeyUp(keyCode, event); 251 | } 252 | 253 | // For testing commands to the motors via ADB. 254 | private boolean handleKeyCode(int keyCode) { 255 | if (mCarController != null) { 256 | switch (keyCode) { 257 | case KeyEvent.KEYCODE_DPAD_UP: //19 258 | mCarController.onCarCommand(CarCommands.GO_FORWARD); 259 | return true; 260 | case KeyEvent.KEYCODE_DPAD_DOWN: //20 261 | mCarController.onCarCommand(CarCommands.GO_BACK); 262 | return true; 263 | case KeyEvent.KEYCODE_DPAD_LEFT: //21 264 | mCarController.onCarCommand(CarCommands.TURN_LEFT); 265 | return true; 266 | case KeyEvent.KEYCODE_DPAD_RIGHT: //22 267 | mCarController.onCarCommand(CarCommands.TURN_RIGHT); 268 | return true; 269 | case KeyEvent.KEYCODE_DPAD_CENTER: //23 270 | mCarController.onCarCommand(CarCommands.STOP); 271 | return true; 272 | } 273 | } 274 | return false; 275 | } 276 | 277 | @Override 278 | public void onGoogleApiConnected(Bundle bundle) {} 279 | 280 | @Override 281 | public void onGoogleApiConnectionSuspended(int cause) {} 282 | 283 | @Override 284 | public void onGoogleApiConnectionFailed(ConnectionResult connectionResult) { 285 | // We don't have a UI with which to resolve connection issues. 286 | Log.e(TAG, "Google API connection failed: " + connectionResult); 287 | } 288 | 289 | private void setConnection(CompanionConnection connection) { 290 | if (mCompanionConnection != connection) { 291 | if (mCompanionConnection != null) { 292 | mCompanionConnection.getConnectionStateLiveData() 293 | .removeObserver(mConnectionStateObserver); 294 | } 295 | mCompanionConnection = connection; 296 | if (mCompanionConnection != null) { 297 | mCompanionConnection.getConnectionStateLiveData() 298 | .observe(this, mConnectionStateObserver); 299 | } 300 | } 301 | } 302 | 303 | private Observer mConnectionStateObserver = new Observer() { 304 | @Override 305 | public void onChanged(@Nullable Integer integer) { 306 | updateUi(); 307 | } 308 | }; 309 | 310 | private void updateUi() { 311 | if (mCompanionConnection != null) { 312 | if (mCompanionConnection.isConnected()) { 313 | mCarController.setLedColor(TricolorLed.GREEN); 314 | mCarController.display(mAdvertisingInfo.mRobocarId); 315 | } else if (mCompanionConnection.isAuthenticating()) { 316 | mCarController.setLedSequence(mAdvertisingInfo.mLedSequence); 317 | mCarController.display(mCompanionConnection.getAuthToken()); 318 | } 319 | } else { 320 | mCarController.display(mAdvertisingInfo.mRobocarId); 321 | if (mIsAdvertising) { 322 | mCarController.setLedSequence(mAdvertisingInfo.mLedSequence); 323 | } else { 324 | mCarController.setLedColor(TricolorLed.YELLOW); 325 | } 326 | } 327 | } 328 | 329 | private Runnable mDisconnectRunnable = new Runnable() { 330 | @Override 331 | public void run() { 332 | if (mKeyPressed) { 333 | disconnectCompanion(); 334 | } 335 | } 336 | }; 337 | 338 | private Runnable mResetRunnable = new Runnable() { 339 | @Override 340 | public void run() { 341 | if (mKeyPressed) { 342 | reset(); 343 | } 344 | } 345 | }; 346 | 347 | private void disconnectCompanion() { 348 | mNearbyAdvertiser.disconnectCompanion(); 349 | mNearbyAdvertiser.startAdvertising(); 350 | } 351 | 352 | private void reset() { 353 | mNearbyAdvertiser.disconnectCompanion(); 354 | mNearbyAdvertiser.stopAdvertising(); 355 | 356 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 357 | // Remove the saved discoverer info. 358 | PreferenceUtils.clearDicovererInfo(prefs); 359 | mNearbyAdvertiser.setPairedDiscovererInfo(null); 360 | 361 | // Remove pair token from advertising info. 362 | mAdvertisingInfo = new AdvertisingInfo(mAdvertisingInfo.mRobocarId, 363 | mAdvertisingInfo.mLedSequence, null); 364 | PreferenceUtils.saveAdvertisingInfo(prefs, mAdvertisingInfo); 365 | mNearbyAdvertiser.setAdvertisingInfo(mAdvertisingInfo); 366 | 367 | // Start advertising after a delay so the display & LED changes are obvious to the user. 368 | mCarController.display(null); 369 | mResetHandler.postDelayed(new Runnable() { 370 | @Override 371 | public void run() { 372 | mNearbyAdvertiser.startAdvertising(); 373 | } 374 | }, 1500L); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/RobocarAdvertiser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import android.arch.lifecycle.LiveData; 19 | import android.arch.lifecycle.MutableLiveData; 20 | import android.content.SharedPreferences; 21 | import android.os.Bundle; 22 | import android.preference.PreferenceManager; 23 | import android.support.annotation.NonNull; 24 | import android.support.annotation.Nullable; 25 | import android.util.Log; 26 | 27 | import com.example.androidthings.robocar.shared.NearbyConnection.ConnectionState; 28 | import com.example.androidthings.robocar.shared.NearbyConnectionManager; 29 | import com.example.androidthings.robocar.shared.PreferenceUtils; 30 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 31 | import com.example.androidthings.robocar.shared.model.DiscovererInfo; 32 | import com.google.android.gms.common.api.GoogleApiClient; 33 | import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 34 | import com.google.android.gms.common.api.ResultCallback; 35 | import com.google.android.gms.common.api.Status; 36 | import com.google.android.gms.nearby.Nearby; 37 | import com.google.android.gms.nearby.connection.AdvertisingOptions; 38 | import com.google.android.gms.nearby.connection.ConnectionInfo; 39 | import com.google.android.gms.nearby.connection.ConnectionResolution; 40 | import com.google.android.gms.nearby.connection.Connections; 41 | import com.google.android.gms.nearby.connection.Connections.StartAdvertisingResult; 42 | 43 | public class RobocarAdvertiser extends NearbyConnectionManager implements ConnectionCallbacks { 44 | 45 | private static final String TAG = "RobocarAdvertiser"; 46 | 47 | private AdvertisingInfo mAdvertisingInfo; 48 | private DiscovererInfo mPairedDiscovererInfo; 49 | 50 | private MutableLiveData mAdvertisingLiveData; 51 | private MutableLiveData mCompanionConnectionLiveData; 52 | 53 | public RobocarAdvertiser(GoogleApiClient client) { 54 | super(client); 55 | client.registerConnectionCallbacks(this); 56 | 57 | mAdvertisingLiveData = new MutableLiveData<>(); 58 | mAdvertisingLiveData.setValue(false); 59 | mCompanionConnectionLiveData = new MutableLiveData<>(); 60 | } 61 | 62 | public void setAdvertisingInfo(AdvertisingInfo info) { 63 | if (mAdvertisingInfo != info) { 64 | boolean wasAdvertising = mAdvertisingLiveData.getValue(); 65 | stopAdvertising(); 66 | 67 | mAdvertisingInfo = info; 68 | if (wasAdvertising) { 69 | startAdvertising(); 70 | } 71 | } 72 | } 73 | 74 | public void setPairedDiscovererInfo(DiscovererInfo info) { 75 | mPairedDiscovererInfo = info; 76 | } 77 | 78 | // For observers 79 | 80 | public LiveData getAdvertisingLiveData() { 81 | return mAdvertisingLiveData; 82 | } 83 | 84 | public LiveData getCompanionConnectionLiveData() { 85 | return mCompanionConnectionLiveData; 86 | } 87 | 88 | // Advertising 89 | 90 | public final void startAdvertising() { 91 | if (!mGoogleApiClient.isConnected()) { 92 | Log.d(TAG, "Google Api Client not connected"); 93 | return; 94 | } 95 | if (mAdvertisingInfo == null) { 96 | Log.d(TAG, "Can't start advertising, no advertising info."); 97 | return; 98 | } 99 | if (mAdvertisingLiveData.getValue()) { 100 | Log.d(TAG, "Already advertising"); 101 | return; 102 | } 103 | 104 | // Pre-emptively set this so the check above catches calls while we wait for a result. 105 | mAdvertisingLiveData.setValue(true); 106 | Nearby.Connections.startAdvertising(mGoogleApiClient, mAdvertisingInfo.getAdvertisingName(), 107 | SERVICE_ID, mLifecycleCallback, new AdvertisingOptions(STRATEGY)) 108 | .setResultCallback(new ResultCallback() { 109 | @Override 110 | public void onResult(@NonNull StartAdvertisingResult startAdvertisingResult) { 111 | Status status = startAdvertisingResult.getStatus(); 112 | if (status.isSuccess()) { 113 | Log.d(TAG, "Advertising started."); 114 | mAdvertisingLiveData.setValue(true); 115 | } else { 116 | Log.d(TAG, String.format("Failed to start advertising. %d, %s", 117 | status.getStatusCode(), status.getStatusMessage())); 118 | // revert state 119 | mAdvertisingLiveData.setValue(false); 120 | } 121 | } 122 | }); 123 | } 124 | 125 | public final void stopAdvertising() { 126 | if (mAdvertisingLiveData.getValue()) { 127 | mAdvertisingLiveData.setValue(false); 128 | // if we're not connected, we should already have lost advertising 129 | if (mGoogleApiClient.isConnected()) { 130 | Nearby.Connections.stopAdvertising(mGoogleApiClient); 131 | } 132 | } 133 | } 134 | 135 | // GoogleApiClient connection 136 | 137 | @Override 138 | public void onConnected(@Nullable Bundle bundle) { 139 | startAdvertising(); 140 | } 141 | 142 | @Override 143 | public void onConnectionSuspended(int cause) { 144 | stopAdvertising(); 145 | disconnectCompanion(); 146 | } 147 | 148 | // Nearby connection 149 | 150 | @Override 151 | protected void onNearbyConnectionInitiated(final String endpointId, 152 | ConnectionInfo connectionInfo) { 153 | super.onNearbyConnectionInitiated(endpointId, connectionInfo); 154 | if (mCompanionConnectionLiveData.getValue() != null) { 155 | // We already have a companion trying to connect. Reject this one. 156 | Nearby.Connections.rejectConnection(mGoogleApiClient, endpointId); 157 | return; 158 | } 159 | 160 | 161 | DiscovererInfo info = DiscovererInfo.parse(connectionInfo.getEndpointName()); 162 | if (info == null || isNotTheDroidWeAreLookingFor(info)) { 163 | // Discoverer looks malformed, or doesn't match our previous paired companion. 164 | Nearby.Connections.rejectConnection(mGoogleApiClient, endpointId); 165 | return; 166 | } 167 | 168 | // Store the endpoint and accept. 169 | CompanionConnection connection = new CompanionConnection(endpointId, info, this); 170 | connection.setAuthToken(connectionInfo.getAuthenticationToken()); 171 | connection.setState(ConnectionState.AUTH_ACCEPTED); 172 | mCompanionConnectionLiveData.setValue(connection); 173 | 174 | Nearby.Connections.acceptConnection(mGoogleApiClient, endpointId, mInternalPayloadListener) 175 | .setResultCallback(new ResultCallback() { 176 | @Override 177 | public void onResult(@NonNull Status status) { 178 | if (status.isSuccess()) { 179 | Log.d(TAG, "Accepted connection. " + endpointId); 180 | // TODO implement a timeout 181 | } else { 182 | Log.d(TAG, "Accept connection failed." + endpointId); 183 | // revert state 184 | clearCompanionEndpoint(); 185 | } 186 | } 187 | }); 188 | } 189 | 190 | @Override 191 | protected void onNearbyConnected(String endpointId, ConnectionResolution connectionResolution) { 192 | super.onNearbyConnected(endpointId, connectionResolution); 193 | if (isCompanionEndpointId(endpointId)) { 194 | stopAdvertising(); 195 | 196 | CompanionConnection connection = mCompanionConnectionLiveData.getValue(); 197 | connection.setState(ConnectionState.CONNECTED); 198 | savePairingInformation(connection); 199 | } else { 200 | disconnectFromEndpoint(endpointId); 201 | } 202 | } 203 | 204 | @Override 205 | protected void onNearbyConnectionRejected(String endpointId) { 206 | super.onNearbyConnectionRejected(endpointId); 207 | if (isCompanionEndpointId(endpointId)) { 208 | clearCompanionEndpoint(); 209 | } 210 | } 211 | 212 | @Override 213 | protected void onNearbyDisconnected(String endpointId) { 214 | super.onNearbyDisconnected(endpointId); 215 | if (isCompanionEndpointId(endpointId)) { 216 | mCompanionConnectionLiveData.getValue().setState(ConnectionState.NOT_CONNECTED); 217 | clearCompanionEndpoint(); 218 | startAdvertising(); 219 | } 220 | } 221 | 222 | private boolean isCompanionEndpointId(String id) { 223 | CompanionConnection connection = mCompanionConnectionLiveData.getValue(); 224 | return connection != null && connection.endpointMatches(id); 225 | } 226 | 227 | private void clearCompanionEndpoint() { 228 | mCompanionConnectionLiveData.setValue(null); 229 | } 230 | 231 | public void disconnectCompanion() { 232 | CompanionConnection connection = mCompanionConnectionLiveData.getValue(); 233 | if (connection != null && connection.isConnected()) { 234 | // Disconnect from our companion. 235 | // If the API client isn't connected, we should have already lost the Nearby connection. 236 | if (mGoogleApiClient.isConnected()) { 237 | disconnectFromEndpoint(connection.getEndpointId()); 238 | } 239 | } 240 | clearCompanionEndpoint(); 241 | } 242 | 243 | private boolean isNotTheDroidWeAreLookingFor(DiscovererInfo info) { 244 | return mPairedDiscovererInfo != null && !mPairedDiscovererInfo.equals(info); 245 | } 246 | 247 | private void savePairingInformation(CompanionConnection connection) { 248 | DiscovererInfo di = connection.getDiscovererInfo(); 249 | String authToken = connection.getAuthToken(); 250 | DiscovererInfo diWithToken = new DiscovererInfo(di.mCompanionId, authToken); 251 | AdvertisingInfo aiWithToken = new AdvertisingInfo(mAdvertisingInfo.mRobocarId, 252 | mAdvertisingInfo.mLedSequence, authToken); 253 | 254 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( 255 | mGoogleApiClient.getContext()); 256 | PreferenceUtils.saveDiscovererInfo(prefs, diWithToken); 257 | PreferenceUtils.saveAdvertisingInfo(prefs, aiWithToken); 258 | 259 | setAdvertisingInfo(aiWithToken); 260 | setPairedDiscovererInfo(diWithToken); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/RobocarViewModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import android.app.Application; 19 | import android.arch.lifecycle.AndroidViewModel; 20 | 21 | import com.example.androidthings.robocar.shared.NearbyConnectionManager; 22 | import com.google.android.gms.common.api.GoogleApiClient; 23 | 24 | 25 | public class RobocarViewModel extends AndroidViewModel { 26 | 27 | private GoogleApiClient mGoogleApiClient; 28 | private RobocarAdvertiser mRobocarAdvertiser; 29 | 30 | public RobocarViewModel(Application application) { 31 | super(application); 32 | mGoogleApiClient = NearbyConnectionManager.createNearbyApiClient(application); 33 | mRobocarAdvertiser = new RobocarAdvertiser(mGoogleApiClient); 34 | } 35 | 36 | public GoogleApiClient getGoogleApiClient() { 37 | return mGoogleApiClient; 38 | } 39 | 40 | public RobocarAdvertiser getRobocarAdvertiser() { 41 | return mRobocarAdvertiser; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androidthings/robocar/TricolorLed.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar; 17 | 18 | import android.support.annotation.IntDef; 19 | import android.util.Log; 20 | 21 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo.LedColor; 22 | import com.google.android.things.pio.Gpio; 23 | import com.google.android.things.pio.PeripheralManager; 24 | 25 | import java.io.IOException; 26 | import java.lang.annotation.Retention; 27 | import java.lang.annotation.RetentionPolicy; 28 | import java.util.HashMap; 29 | import java.util.Map; 30 | 31 | public class TricolorLed implements AutoCloseable { 32 | 33 | private static final String TAG = "TricolorLed"; 34 | 35 | public static final int OFF = 0; 36 | public static final int RED = 1; // __R 37 | public static final int GREEN = 2; // _G_ 38 | public static final int YELLOW = 3; // _GR 39 | public static final int BLUE = 4; // B__ 40 | public static final int MAGENTA = 5; // B_R 41 | public static final int CYAN = 6; // BG_ 42 | public static final int WHITE = 7; // BGR 43 | 44 | @IntDef({OFF, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE}) 45 | @Retention(RetentionPolicy.SOURCE) 46 | public @interface Tricolor{} 47 | 48 | private static final Map MAP; 49 | static { 50 | MAP = new HashMap<>(); 51 | MAP.put(LedColor.RED, RED); 52 | MAP.put(LedColor.GREEN, GREEN); 53 | MAP.put(LedColor.YELLOW, YELLOW); 54 | MAP.put(LedColor.BLUE, BLUE); 55 | MAP.put(LedColor.MAGENTA, MAGENTA); 56 | MAP.put(LedColor.CYAN, CYAN); 57 | MAP.put(LedColor.WHITE, WHITE); 58 | } 59 | 60 | public static @Tricolor int ledColorToTricolor(LedColor color) { 61 | Integer v = MAP.get(color); 62 | return v == null ? OFF : v; 63 | } 64 | 65 | private Gpio mGpioRed; 66 | private Gpio mGpioGreen; 67 | private Gpio mGpioBlue; 68 | 69 | private @Tricolor int mColor = OFF; 70 | 71 | public TricolorLed(String redPin, String greenPin, String bluePin) { 72 | mGpioRed = createGpio(redPin); 73 | mGpioGreen = createGpio(greenPin); 74 | mGpioBlue = createGpio(bluePin); 75 | } 76 | 77 | private Gpio createGpio(String pin) { 78 | try { 79 | Gpio gpio = PeripheralManager.getInstance().openGpio(pin); 80 | gpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_HIGH); 81 | return gpio; 82 | } catch (IOException e) { 83 | Log.e(TAG, "Error creating GPIO for pin " + pin, e); 84 | } 85 | return null; 86 | } 87 | 88 | @Override 89 | public void close() throws Exception { 90 | setColor(OFF); 91 | closeGpio(mGpioRed); 92 | closeGpio(mGpioGreen); 93 | closeGpio(mGpioBlue); 94 | mGpioRed = mGpioGreen = mGpioBlue = null; 95 | } 96 | 97 | private void closeGpio(Gpio gpio) { 98 | if (gpio != null) { 99 | try { 100 | gpio.close(); 101 | } catch (IOException e) { 102 | Log.e(TAG, "Error closing gpio", e); 103 | } 104 | } 105 | } 106 | 107 | public @Tricolor int getColor() { 108 | return mColor; 109 | } 110 | 111 | public void setColor(@Tricolor int color) { 112 | // only care about the 3 LSBs 113 | mColor = color & WHITE; 114 | // Common-Anode uses LOW to activate the color, so unset bits are set HIGH 115 | setGpioValue(mGpioRed, (color & 1) == 0); 116 | setGpioValue(mGpioGreen, (color & 2) == 0); 117 | setGpioValue(mGpioBlue, (color & 4) == 0); 118 | } 119 | 120 | private void setGpioValue(Gpio gpio, boolean value) { 121 | if (gpio != null) { 122 | try { 123 | gpio.setValue(value); 124 | } catch (IOException ignored) { 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Robocar 3 | 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.0.1' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /companion/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /companion/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | android { 20 | compileSdkVersion 27 21 | 22 | defaultConfig { 23 | applicationId "com.example.androidthings.robocar.companion" 24 | minSdkVersion 24 25 | targetSdkVersion 27 26 | versionCode 1 27 | versionName "1.0" 28 | 29 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 30 | 31 | } 32 | buildTypes { 33 | release { 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 42 | exclude group: 'com.android.support', module: 'support-annotations' 43 | }) 44 | 45 | testImplementation 'junit:junit:4.12' 46 | 47 | implementation 'com.android.support:appcompat-v7:27.0.2' 48 | implementation 'com.android.support:recyclerview-v7:27.0.2' 49 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 50 | implementation project(path: ':shared') 51 | } 52 | -------------------------------------------------------------------------------- /companion/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/google/home/alexlucas/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /companion/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/CompanionActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.androidthings.robocar.companion; 18 | 19 | import android.arch.lifecycle.Observer; 20 | import android.arch.lifecycle.ViewModelProviders; 21 | import android.content.Intent; 22 | import android.content.IntentSender.SendIntentException; 23 | import android.content.SharedPreferences; 24 | import android.os.Bundle; 25 | import android.preference.PreferenceManager; 26 | import android.support.annotation.Nullable; 27 | import android.support.v4.app.Fragment; 28 | import android.support.v4.app.FragmentManager; 29 | import android.support.v7.app.AppCompatActivity; 30 | import android.util.Log; 31 | 32 | import com.example.androidthings.robocar.companion.CompanionViewModel.NavigationState; 33 | import com.example.androidthings.robocar.shared.ConnectorFragment; 34 | import com.example.androidthings.robocar.shared.ConnectorFragment.ConnectorCallbacks; 35 | import com.example.androidthings.robocar.shared.PreferenceUtils; 36 | import com.example.androidthings.robocar.shared.model.DiscovererInfo; 37 | import com.google.android.gms.common.ConnectionResult; 38 | 39 | 40 | public class CompanionActivity extends AppCompatActivity implements ConnectorCallbacks { 41 | 42 | private static final String TAG = "CompanionActivity"; 43 | 44 | private static final String FRAGMENT_TAG_DISCOVERY = "fragment.robocar_discovery"; 45 | private static final String FRAGMENT_TAG_CONTROLLER = "fragment.robocar_controller"; 46 | private static final String SAVEDSTATE_FRAGMENT_TAG = "savedstate.fragment_tag"; 47 | 48 | private static final int REQUEST_RESOLVE_CONNECTION = 1; 49 | 50 | private CompanionViewModel mViewModel; 51 | 52 | private RobocarDiscoveryFragment mDiscoveryFragment; 53 | private ControllerFragment mControllerFragment; 54 | private Fragment mCurrentFragment; 55 | private String mCurrentFragmentTag; 56 | 57 | private DiscovererInfo mDiscovererInfo; 58 | 59 | @Override 60 | protected void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | setContentView(R.layout.activity_companion); 63 | 64 | // init DiscovererInfo 65 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 66 | mDiscovererInfo = PreferenceUtils.loadDiscovererInfo(prefs); 67 | if (mDiscovererInfo == null) { 68 | mDiscovererInfo = DiscovererInfo.generateDiscoveryInfo(); 69 | PreferenceUtils.saveDiscovererInfo(prefs, mDiscovererInfo); 70 | } 71 | 72 | mViewModel = ViewModelProviders.of(this).get(CompanionViewModel.class); 73 | 74 | if (savedInstanceState == null) { 75 | // First launch. Attach the connector fragment and give it our client to connect. 76 | ConnectorFragment.attachTo(this, mViewModel.getGoogleApiClient()); 77 | } else { 78 | // Re-acquire references to attached fragments. 79 | FragmentManager fm = getSupportFragmentManager(); 80 | mDiscoveryFragment = 81 | (RobocarDiscoveryFragment) fm.findFragmentByTag(FRAGMENT_TAG_DISCOVERY); 82 | mControllerFragment = 83 | (ControllerFragment) fm.findFragmentByTag(FRAGMENT_TAG_CONTROLLER); 84 | 85 | mCurrentFragmentTag = savedInstanceState.getString(SAVEDSTATE_FRAGMENT_TAG); 86 | if (FRAGMENT_TAG_DISCOVERY.equals(mCurrentFragmentTag)) { 87 | mCurrentFragment = mDiscoveryFragment; 88 | } else if (FRAGMENT_TAG_CONTROLLER.equals(mCurrentFragmentTag)) { 89 | mCurrentFragment = mControllerFragment; 90 | } 91 | } 92 | 93 | RobocarDiscoverer discoverer = mViewModel.getRobocarDiscoverer(); 94 | discoverer.setDiscovererInfo(mDiscovererInfo); 95 | discoverer.setPairedAdvertisingInfo(PreferenceUtils.loadAdvertisingInfo(prefs)); 96 | mViewModel.getNavigationStateLiveData().observe(this, new Observer() { 97 | @Override 98 | public void onChanged(@Nullable Integer value) { 99 | assert value != null; 100 | if (value == NavigationState.DISCOVERY_UI) { 101 | showDiscoveryUi(); 102 | } else if (value == NavigationState.CONTROLLER_UI) { 103 | showControllerUi(); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | @Override 110 | public void onSaveInstanceState(Bundle outState) { 111 | super.onSaveInstanceState(outState); 112 | outState.putString(SAVEDSTATE_FRAGMENT_TAG, mCurrentFragmentTag); 113 | } 114 | 115 | private void showDiscoveryUi() { 116 | if (mDiscoveryFragment == null) { 117 | mDiscoveryFragment = new RobocarDiscoveryFragment(); 118 | } 119 | swapFragment(mDiscoveryFragment, FRAGMENT_TAG_DISCOVERY); 120 | } 121 | 122 | private void showControllerUi() { 123 | if (mControllerFragment == null) { 124 | mControllerFragment = new ControllerFragment(); 125 | } 126 | swapFragment(mControllerFragment, FRAGMENT_TAG_CONTROLLER); 127 | } 128 | 129 | private void swapFragment(Fragment fragment, String tag) { 130 | if (mCurrentFragment != fragment) { 131 | getSupportFragmentManager().beginTransaction() 132 | .replace(R.id.fragment_container, fragment, tag) 133 | .commit(); 134 | mCurrentFragment = fragment; 135 | mCurrentFragmentTag = tag; 136 | } 137 | } 138 | 139 | @Override 140 | public void onBackPressed() { 141 | if (mCurrentFragment == mControllerFragment) { 142 | mControllerFragment.disconnect(); 143 | } else { 144 | super.onBackPressed(); 145 | } 146 | } 147 | 148 | @Override 149 | public void onGoogleApiConnected(Bundle bundle) {} 150 | 151 | @Override 152 | public void onGoogleApiConnectionSuspended(int cause) {} 153 | 154 | @Override 155 | public void onGoogleApiConnectionFailed(ConnectionResult connectionResult) { 156 | if (connectionResult.hasResolution()) { 157 | try { 158 | connectionResult.startResolutionForResult(this, REQUEST_RESOLVE_CONNECTION); 159 | } catch (SendIntentException e) { 160 | Log.e(TAG, "Google API connection failed. " + connectionResult, e); 161 | } 162 | } else { 163 | Log.e(TAG, "Google API connection failed. " + connectionResult); 164 | } 165 | } 166 | 167 | @Override 168 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 169 | if (requestCode == REQUEST_RESOLVE_CONNECTION) { 170 | if (resultCode == RESULT_OK) { 171 | // try to reconnect 172 | ConnectorFragment.connect(this); 173 | } 174 | } else { 175 | super.onActivityResult(requestCode, resultCode, data); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/CompanionViewModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.app.Application; 19 | import android.arch.lifecycle.AndroidViewModel; 20 | import android.arch.lifecycle.LiveData; 21 | import android.arch.lifecycle.MutableLiveData; 22 | import android.support.annotation.IntDef; 23 | 24 | import com.example.androidthings.robocar.shared.NearbyConnectionManager; 25 | import com.google.android.gms.common.api.GoogleApiClient; 26 | 27 | 28 | public class CompanionViewModel extends AndroidViewModel { 29 | 30 | @IntDef({NavigationState.DISCOVERY_UI, NavigationState.CONTROLLER_UI}) 31 | public @interface NavigationState { 32 | int DISCOVERY_UI = 1; 33 | int CONTROLLER_UI = 2; 34 | } 35 | 36 | private final GoogleApiClient mGoogleApiClient; 37 | private final RobocarDiscoverer mRobocarDiscoverer; 38 | 39 | private final MutableLiveData mNavigationState; 40 | 41 | public CompanionViewModel(Application application) { 42 | super(application); 43 | mGoogleApiClient = NearbyConnectionManager.createNearbyApiClient(application); 44 | mRobocarDiscoverer = new RobocarDiscoverer(mGoogleApiClient); 45 | 46 | mNavigationState = new MutableLiveData<>(); 47 | mNavigationState.setValue(NavigationState.DISCOVERY_UI); 48 | } 49 | 50 | public GoogleApiClient getGoogleApiClient() { 51 | return mGoogleApiClient; 52 | } 53 | 54 | public RobocarDiscoverer getRobocarDiscoverer() { 55 | return mRobocarDiscoverer; 56 | } 57 | 58 | public LiveData getNavigationStateLiveData() { 59 | return mNavigationState; 60 | } 61 | 62 | public void navigateTo(@NavigationState int state) { 63 | mNavigationState.setValue(state); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/ControllerFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.arch.lifecycle.Observer; 19 | import android.arch.lifecycle.ViewModelProviders; 20 | import android.os.Bundle; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.app.Fragment; 23 | import android.util.Log; 24 | import android.util.SparseArray; 25 | import android.view.LayoutInflater; 26 | import android.view.Menu; 27 | import android.view.MenuInflater; 28 | import android.view.MenuItem; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.TextView; 32 | 33 | import com.example.androidthings.robocar.companion.CompanionViewModel.NavigationState; 34 | import com.example.androidthings.robocar.shared.CarCommands; 35 | import com.google.android.gms.nearby.connection.Payload; 36 | import com.google.android.gms.nearby.connection.PayloadCallback; 37 | import com.google.android.gms.nearby.connection.PayloadTransferUpdate; 38 | 39 | 40 | public class ControllerFragment extends Fragment { 41 | 42 | private static final String TAG = "ControllerFragment"; 43 | 44 | private SparseArray mCarControlMap = new SparseArray<>(5); 45 | private View mActivatedControl; 46 | private View mErrorView; 47 | private TextView mLogView; 48 | 49 | private CompanionViewModel mViewModel; 50 | private RobocarDiscoverer mRobocarDiscoverer; 51 | private RobocarConnection mRobocarConnection; 52 | 53 | @Override 54 | public void onCreate(@Nullable Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | setHasOptionsMenu(true); 57 | } 58 | 59 | @Nullable 60 | @Override 61 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 62 | @Nullable Bundle savedInstanceState) { 63 | return inflater.inflate(R.layout.fragment_controller, container, false); 64 | } 65 | 66 | @Override 67 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 68 | super.onViewCreated(view, savedInstanceState); 69 | mErrorView = view.findViewById(R.id.error); 70 | mLogView = view.findViewById(R.id.log_text); 71 | 72 | configureButton(view, R.id.btn_forward, CarCommands.GO_FORWARD); 73 | configureButton(view, R.id.btn_back, CarCommands.GO_BACK); 74 | configureButton(view, R.id.btn_left, CarCommands.TURN_LEFT); 75 | configureButton(view, R.id.btn_right, CarCommands.TURN_RIGHT); 76 | configureButton(view, R.id.btn_stop, CarCommands.STOP); 77 | } 78 | 79 | @Override 80 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 81 | super.onActivityCreated(savedInstanceState); 82 | mViewModel = ViewModelProviders.of(getActivity()).get(CompanionViewModel.class); 83 | mRobocarDiscoverer = mViewModel.getRobocarDiscoverer(); 84 | 85 | mRobocarDiscoverer.getRobocarConnectionLiveData().observe(this, 86 | new Observer() { 87 | @Override 88 | public void onChanged(@Nullable RobocarConnection connection) { 89 | mRobocarConnection = connection; 90 | if (connection == null || !connection.isConnected()) { 91 | // We're not connected, so go back to discovery UI 92 | mViewModel.navigateTo(NavigationState.DISCOVERY_UI); 93 | } 94 | } 95 | }); 96 | } 97 | 98 | @Override 99 | public void onStart() { 100 | super.onStart(); 101 | mRobocarDiscoverer.setPayloadListener(mPayloadListener); 102 | } 103 | 104 | @Override 105 | public void onStop() { 106 | super.onStop(); 107 | mRobocarDiscoverer.setPayloadListener(null); 108 | } 109 | 110 | @Override 111 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 112 | super.onCreateOptionsMenu(menu, inflater); 113 | inflater.inflate(R.menu.controller, menu); 114 | } 115 | 116 | @Override 117 | public boolean onOptionsItemSelected(MenuItem item) { 118 | if (item.getItemId() == R.id.action_disconnect) { 119 | disconnect(); 120 | return true; 121 | } 122 | return super.onOptionsItemSelected(item); 123 | } 124 | 125 | public void disconnect() { 126 | if (mRobocarConnection != null) { 127 | mRobocarConnection.disconnect(); 128 | } 129 | } 130 | 131 | void configureButton(View view, int buttonId, final byte command) { 132 | View button = view.findViewById(buttonId); 133 | if (button != null) { 134 | mCarControlMap.append(command, button); 135 | button.setOnClickListener(new View.OnClickListener() { 136 | @Override 137 | public void onClick(View v) { 138 | mRobocarConnection.sendCommand(command); 139 | setActivatedControl(v); 140 | } 141 | }); 142 | } 143 | } 144 | 145 | private void setActivatedControl(View view) { 146 | if (mActivatedControl != null && mActivatedControl != view) { 147 | mActivatedControl.setActivated(false); 148 | } 149 | mActivatedControl = view; 150 | if (mActivatedControl != null) { 151 | mActivatedControl.setActivated(true); 152 | } 153 | } 154 | 155 | private void logUi(String text) { 156 | if (mLogView.getText().length() > 0) { 157 | mLogView.append("\n"); 158 | } 159 | mLogView.append(text); 160 | } 161 | 162 | PayloadCallback mPayloadListener = new PayloadCallback() { 163 | @Override 164 | public void onPayloadReceived(String s, Payload payload) { 165 | byte[] bytes = CarCommands.fromPayload(payload); 166 | if (bytes == null || bytes.length == 0) { 167 | return; 168 | } 169 | 170 | byte command = bytes[0]; 171 | if (command == CarCommands.ERROR) { 172 | mErrorView.setVisibility(View.VISIBLE); 173 | Log.d(TAG, "onPayloadReceived: error"); 174 | } else { 175 | mErrorView.setVisibility(View.GONE); 176 | Log.d(TAG, "onPayloadReceived: " + command); 177 | // activate control 178 | View toActivate = mCarControlMap.get(command); 179 | setActivatedControl(toActivate); 180 | } 181 | } 182 | 183 | @Override 184 | public void onPayloadTransferUpdate(String endpoitnId, PayloadTransferUpdate xferUpdate) { 185 | } 186 | }; 187 | } 188 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import com.example.androidthings.robocar.shared.NearbyConnection; 19 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 20 | 21 | /** 22 | * Handle for a connection to a Robocar, providing convenient methods for both authenticating the 23 | * connection and transmitting data through it. 24 | */ 25 | public class RobocarConnection extends NearbyConnection { 26 | 27 | private final RobocarDiscoverer mRobocarDiscoverer; 28 | private final AdvertisingInfo mAdvertisingInfo; 29 | 30 | private final boolean mAutoConnect; 31 | 32 | public RobocarConnection(String endpointId, AdvertisingInfo advertisingInfo, 33 | RobocarDiscoverer robocarDiscoverer, boolean autoConnect) { 34 | super(endpointId, robocarDiscoverer); 35 | if (advertisingInfo == null) { 36 | throw new IllegalArgumentException("AdvertisingInfo cannot be null"); 37 | } 38 | mAdvertisingInfo = advertisingInfo; 39 | mRobocarDiscoverer = robocarDiscoverer; 40 | mAutoConnect = autoConnect; 41 | } 42 | 43 | public AdvertisingInfo getAdvertisingInfo() { 44 | return mAdvertisingInfo; 45 | } 46 | 47 | public boolean isAutoConnect() { 48 | return mAutoConnect; 49 | } 50 | 51 | public void accept() { 52 | if (getState() == ConnectionState.AUTHENTICATING) { 53 | mRobocarDiscoverer.acceptConnection(); 54 | } 55 | } 56 | 57 | public void reject() { 58 | if (getState() == ConnectionState.AUTHENTICATING) { 59 | mRobocarDiscoverer.rejectConnection(); 60 | } 61 | } 62 | 63 | public void disconnect() { 64 | mRobocarDiscoverer.disconnect(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarConnectionDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.annotation.SuppressLint; 19 | import android.app.Dialog; 20 | import android.arch.lifecycle.Observer; 21 | import android.arch.lifecycle.ViewModelProviders; 22 | import android.content.DialogInterface; 23 | import android.os.Bundle; 24 | import android.support.annotation.NonNull; 25 | import android.support.annotation.Nullable; 26 | import android.support.v4.app.DialogFragment; 27 | import android.support.v7.app.AlertDialog; 28 | import android.view.View; 29 | import android.view.View.OnClickListener; 30 | import android.widget.TextView; 31 | 32 | import com.example.androidthings.robocar.companion.CompanionViewModel.NavigationState; 33 | import com.example.androidthings.robocar.shared.NearbyConnection.ConnectionState; 34 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 35 | 36 | 37 | /** 38 | * DialogFragment used for connecting to a Robocar. 39 | */ 40 | public class RobocarConnectionDialog extends DialogFragment implements OnClickListener { 41 | 42 | private TextView mMessageText; 43 | private View mPositiveButton; 44 | private View mNegativeButton; 45 | private View mProgressContainer; 46 | private TextView mProgressText; 47 | 48 | private CompanionViewModel mViewModel; 49 | private RobocarConnection mRobocarConnection; 50 | 51 | @ConnectionState 52 | private int mState = ConnectionState.NOT_CONNECTED; 53 | 54 | @NonNull 55 | @Override 56 | public Dialog onCreateDialog(Bundle savedInstanceState) { 57 | AlertDialog dialog = new AlertDialog.Builder(getContext()) 58 | .setTitle(R.string.dialog_title_connect_to_robocar) 59 | .create(); 60 | // Inflate the content separately so that we can use findViewById() 61 | @SuppressLint("InflateParams") 62 | View view = dialog.getLayoutInflater().inflate(R.layout.fragment_auth_dialog, null, false); 63 | dialog.setView(view); 64 | dialog.setCanceledOnTouchOutside(false); // can still cancel with Back 65 | 66 | mMessageText = view.findViewById(android.R.id.message); 67 | mPositiveButton = view.findViewById(R.id.positive_button); 68 | mNegativeButton = view.findViewById(R.id.negative_button); 69 | mProgressContainer = view.findViewById(R.id.progress_container); 70 | mProgressText = view.findViewById(R.id.progress_text); 71 | 72 | mPositiveButton.setOnClickListener(this); 73 | mNegativeButton.setOnClickListener(this); 74 | 75 | return dialog; 76 | } 77 | 78 | @Override 79 | public void onActivityCreated(Bundle savedInstanceState) { 80 | super.onActivityCreated(savedInstanceState); 81 | 82 | mViewModel = ViewModelProviders.of(getActivity()).get(CompanionViewModel.class); 83 | mRobocarConnection = mViewModel.getRobocarDiscoverer() 84 | .getRobocarConnectionLiveData().getValue(); 85 | if (mRobocarConnection == null || mRobocarConnection.isConnected()) { 86 | // Don't need to show 87 | dismiss(); 88 | return; 89 | } 90 | 91 | mRobocarConnection.getConnectionStateLiveData().observe(this, new Observer() { 92 | @Override 93 | public void onChanged(@Nullable Integer value) { 94 | //noinspection ConstantConditions 95 | setState(value); 96 | } 97 | }); 98 | 99 | AdvertisingInfo info = mRobocarConnection.getAdvertisingInfo(); 100 | getDialog().setTitle(getString(R.string.dialog_title_connect_to_robocar, 101 | info.mRobocarId)); 102 | updateUi(); 103 | } 104 | 105 | private void setState(@ConnectionState int state) { 106 | if (mState == state) { 107 | return; 108 | } 109 | 110 | if (state == ConnectionState.CONNECTED) { 111 | // TODO save Robocar info for automatic reconnect 112 | dismiss(); 113 | mViewModel.navigateTo(NavigationState.CONTROLLER_UI); 114 | return; 115 | } else if (state == ConnectionState.NOT_CONNECTED) { 116 | // TODO trigger a Snackbar to show error message based on the prior state 117 | dismiss(); 118 | mViewModel.navigateTo(NavigationState.DISCOVERY_UI); 119 | return; 120 | } 121 | 122 | mState = state; 123 | updateUi(); 124 | } 125 | 126 | private void updateUi() { 127 | if (mState == ConnectionState.REQUESTING) { 128 | mMessageText.setVisibility(View.GONE); 129 | mPositiveButton.setVisibility(View.GONE); 130 | mNegativeButton.setVisibility(View.GONE); 131 | mProgressContainer.setVisibility(View.VISIBLE); 132 | mProgressText.setText(R.string.dialog_message_requesting_connection); 133 | } else if (mState == ConnectionState.AUTHENTICATING 134 | || mState == ConnectionState.AUTH_ACCEPTED) { 135 | showMessageText(); 136 | boolean showProgress = mState == ConnectionState.AUTH_ACCEPTED; 137 | mPositiveButton.setVisibility(showProgress ? View.GONE : View.VISIBLE); 138 | mNegativeButton.setVisibility(showProgress ? View.GONE : View.VISIBLE); 139 | mProgressContainer.setVisibility(showProgress ? View.VISIBLE : View.GONE); 140 | mProgressText.setText(R.string.dialog_message_authenticating); 141 | } 142 | } 143 | 144 | private void showMessageText() { 145 | AdvertisingInfo info = mRobocarConnection.getAdvertisingInfo(); 146 | mMessageText.setText(getString(R.string.dialog_message_auth_instructions, 147 | info.mRobocarId, 148 | AdvertisingInfo.ledColorsToString(info.mLedSequence), 149 | mRobocarConnection.getAuthToken())); 150 | mMessageText.setVisibility(View.VISIBLE); 151 | } 152 | 153 | @Override 154 | public void onClick(View view) { 155 | switch (view.getId()) { 156 | case R.id.positive_button: 157 | mRobocarConnection.accept(); 158 | break; 159 | case R.id.negative_button: 160 | dismiss(); 161 | mRobocarConnection.reject(); 162 | break; 163 | } 164 | } 165 | 166 | @Override 167 | public void onCancel(DialogInterface dialog) { 168 | super.onCancel(dialog); 169 | // We shouldn't be showing if we're connected, but in case we are, let's not drop the 170 | // connection since the user should be expecting to see the controls. 171 | if (!mRobocarConnection.isConnected()) { 172 | if (mRobocarConnection.getState() == ConnectionState.AUTHENTICATING) { 173 | mRobocarConnection.reject(); 174 | } else { 175 | // This will clear the connection even if not fully connected. 176 | mRobocarConnection.disconnect(); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarDiscoverer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.arch.lifecycle.LiveData; 19 | import android.arch.lifecycle.MutableLiveData; 20 | import android.content.SharedPreferences; 21 | import android.os.Bundle; 22 | import android.preference.PreferenceManager; 23 | import android.support.annotation.NonNull; 24 | import android.support.annotation.Nullable; 25 | import android.util.Log; 26 | 27 | import com.example.androidthings.robocar.shared.NearbyConnection.ConnectionState; 28 | import com.example.androidthings.robocar.shared.NearbyConnectionManager; 29 | import com.example.androidthings.robocar.shared.PreferenceUtils; 30 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 31 | import com.example.androidthings.robocar.shared.model.DiscovererInfo; 32 | import com.google.android.gms.common.api.GoogleApiClient; 33 | import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 34 | import com.google.android.gms.common.api.PendingResult; 35 | import com.google.android.gms.common.api.ResultCallback; 36 | import com.google.android.gms.common.api.Status; 37 | import com.google.android.gms.nearby.Nearby; 38 | import com.google.android.gms.nearby.connection.ConnectionInfo; 39 | import com.google.android.gms.nearby.connection.ConnectionResolution; 40 | import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo; 41 | import com.google.android.gms.nearby.connection.DiscoveryOptions; 42 | import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback; 43 | 44 | import java.util.ArrayList; 45 | import java.util.LinkedHashMap; 46 | import java.util.List; 47 | import java.util.Map; 48 | 49 | 50 | public class RobocarDiscoverer extends NearbyConnectionManager implements ConnectionCallbacks { 51 | 52 | private static final String TAG = "RobocarDiscoverer"; 53 | 54 | private final Map mEndpointMap = new LinkedHashMap<>(); 55 | private DiscovererInfo mDiscovererInfo; 56 | private AdvertisingInfo mPairedAdvertisingInfo; 57 | 58 | private boolean mAutoConnectEnabled = true; 59 | 60 | private MutableLiveData mDiscoveryLiveData; 61 | private MutableLiveData> mRobocarEndpointsLiveData; 62 | private MutableLiveData mRobocarConnectionLiveData; 63 | 64 | // Discovery 65 | private final EndpointDiscoveryCallback mEndpointDiscoveryCallback = 66 | new EndpointDiscoveryCallback() { 67 | @Override 68 | public void onEndpointFound(String endpointId, 69 | DiscoveredEndpointInfo discoveredEndpointInfo) { 70 | onNearbyEndpointFound(endpointId, discoveredEndpointInfo); 71 | } 72 | 73 | @Override 74 | public void onEndpointLost(String endpointId) { 75 | onNearbyEndpointLost(endpointId); 76 | } 77 | }; 78 | 79 | public RobocarDiscoverer(GoogleApiClient client) { 80 | super(client); 81 | client.registerConnectionCallbacks(this); 82 | 83 | mDiscoveryLiveData = new MutableLiveData<>(); 84 | mDiscoveryLiveData.setValue(false); 85 | mRobocarEndpointsLiveData = new MutableLiveData<>(); 86 | mRobocarConnectionLiveData = new MutableLiveData<>(); 87 | } 88 | 89 | public void setDiscovererInfo(DiscovererInfo info) { 90 | // This is only checked when we request a connection, and we don't need to interrupt one 91 | // already in progress. 92 | mDiscovererInfo = info; 93 | } 94 | 95 | public void setPairedAdvertisingInfo(AdvertisingInfo info) { 96 | mPairedAdvertisingInfo = info; 97 | } 98 | 99 | // For observers 100 | 101 | public LiveData getDiscoveryLiveData() { 102 | return mDiscoveryLiveData; 103 | } 104 | 105 | public LiveData> getRobocarEndpointsLiveData() { 106 | return mRobocarEndpointsLiveData; 107 | } 108 | 109 | public LiveData getRobocarConnectionLiveData() { 110 | return mRobocarConnectionLiveData; 111 | } 112 | 113 | // Discovery 114 | 115 | public void startDiscovery() { 116 | if (!mGoogleApiClient.isConnected()) { 117 | Log.d(TAG, "Google Api Client not connected"); 118 | return; 119 | } 120 | if (mDiscoveryLiveData.getValue()) { 121 | Log.d(TAG, "startDiscovery already called"); 122 | return; 123 | } 124 | 125 | // Pre-emptively set this so the check above catches calls while we wait for a result. 126 | mDiscoveryLiveData.setValue(true); 127 | Nearby.Connections.startDiscovery(mGoogleApiClient, SERVICE_ID, mEndpointDiscoveryCallback, 128 | new DiscoveryOptions(STRATEGY)).setResultCallback(new ResultCallback() { 129 | @Override 130 | public void onResult(@NonNull Status status) { 131 | if (status.isSuccess()) { 132 | Log.d(TAG, "Discovery started."); 133 | mDiscoveryLiveData.setValue(true); 134 | } else { 135 | Log.d(TAG, String.format("Failed to start discovery. %d, %s", 136 | status.getStatusCode(), status.getStatusMessage())); 137 | mDiscoveryLiveData.setValue(false); 138 | } 139 | } 140 | }); 141 | } 142 | 143 | public void stopDiscovery() { 144 | if (mDiscoveryLiveData.getValue()) { 145 | mDiscoveryLiveData.setValue(false); 146 | // if we're not connected, we already should have lost discovery 147 | if (mGoogleApiClient.isConnected()) { 148 | Nearby.Connections.stopDiscovery(mGoogleApiClient); 149 | } 150 | clearEndpoints(); 151 | } 152 | } 153 | 154 | // Google API connection 155 | 156 | @Override 157 | public void onConnected(@Nullable Bundle bundle) { 158 | startDiscovery(); 159 | } 160 | 161 | @Override 162 | public void onConnectionSuspended(int cause) { 163 | stopDiscovery(); 164 | clearRobocarConnection(); 165 | clearEndpoints(); 166 | } 167 | 168 | // Nearby connection 169 | 170 | private void onNearbyEndpointFound(String endpointId, DiscoveredEndpointInfo endpointInfo) { 171 | AdvertisingInfo info = AdvertisingInfo.parseAdvertisingName(endpointInfo.getEndpointName()); 172 | if (info != null) { 173 | boolean isRemembered = isTheDroidWeAreLookingFor(info); 174 | mEndpointMap.put(endpointId, new RobocarEndpoint(endpointId, info, isRemembered)); 175 | onEndpointsChanged(); 176 | 177 | if (isRemembered && mAutoConnectEnabled) { 178 | // try to auto-connect 179 | requestConnection(endpointId); 180 | } 181 | } 182 | } 183 | 184 | private void onNearbyEndpointLost(String endpointId) { 185 | mEndpointMap.remove(endpointId); 186 | onEndpointsChanged(); 187 | } 188 | 189 | public void requestConnection(String endpointId) { 190 | if (mRobocarConnectionLiveData.getValue() != null) { 191 | // We're already connecting to something else 192 | return; 193 | } 194 | RobocarEndpoint endpoint = mEndpointMap.get(endpointId); 195 | if (endpoint == null) { 196 | // Not a valid ID 197 | return; 198 | } 199 | 200 | RobocarConnection connection = new RobocarConnection(endpoint.mEndpointId, 201 | endpoint.mAdvertisingInfo, this, endpoint.mIsRemembered); 202 | connection.setState(ConnectionState.REQUESTING); 203 | mRobocarConnectionLiveData.setValue(connection); 204 | 205 | String name = mDiscovererInfo == null ? null : mDiscovererInfo.getAdvertisingName(); 206 | Nearby.Connections.requestConnection(mGoogleApiClient, name, endpointId, mLifecycleCallback) 207 | .setResultCallback(new ResultCallback() { 208 | @Override 209 | public void onResult(@NonNull Status status) { 210 | if (status.isSuccess()) { 211 | Log.d(TAG, "Requested connection."); 212 | } else { 213 | Log.d(TAG, String.format("Request connection failed. %d %s", 214 | status.getStatusCode(), status.getStatusMessage())); 215 | clearRobocarConnection(); 216 | } 217 | } 218 | }); 219 | } 220 | 221 | @Override 222 | protected void onNearbyConnectionInitiated(String endpointId, ConnectionInfo connectionInfo) { 223 | super.onNearbyConnectionInitiated(endpointId, connectionInfo); 224 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 225 | if (connection != null && connection.endpointMatches(endpointId)) { 226 | connection.setAuthToken(connectionInfo.getAuthenticationToken()); 227 | if (connection.isAutoConnect()) { 228 | acceptConnection(); 229 | } else { 230 | // Wait for something else to call acceptConnection. 231 | connection.setState(ConnectionState.AUTHENTICATING); 232 | } 233 | } else { 234 | // We didn't request this connection, so reject it. 235 | rejectConnection(endpointId); 236 | } 237 | } 238 | 239 | private PendingResult acceptConnection(String endpointId) { 240 | return Nearby.Connections.acceptConnection(mGoogleApiClient, endpointId, 241 | mInternalPayloadListener); 242 | } 243 | 244 | public void acceptConnection() { 245 | final RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 246 | if (connection == null) { 247 | return; 248 | } 249 | 250 | connection.setState(ConnectionState.AUTH_ACCEPTED); 251 | acceptConnection(connection.getEndpointId()) 252 | .setResultCallback(new ResultCallback() { 253 | @Override 254 | public void onResult(@NonNull Status status) { 255 | if (status.isSuccess()) { 256 | Log.d(TAG, "Accepted connection."); 257 | } else { 258 | Log.d(TAG, String.format("Accept unsuccessful. %d %s", 259 | status.getStatusCode(), status.getStatusMessage())); 260 | // revert state 261 | connection.setState(ConnectionState.AUTHENTICATING); 262 | } 263 | } 264 | }); 265 | } 266 | 267 | private PendingResult rejectConnection(String endpointId) { 268 | return Nearby.Connections.rejectConnection(mGoogleApiClient, endpointId); 269 | } 270 | 271 | public void rejectConnection() { 272 | final RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 273 | if (connection == null) { 274 | return; 275 | } 276 | 277 | connection.setState(ConnectionState.AUTH_REJECTED); 278 | rejectConnection(connection.getEndpointId()) 279 | .setResultCallback(new ResultCallback() { 280 | @Override 281 | public void onResult(@NonNull Status status) { 282 | if (status.isSuccess()) { 283 | Log.d(TAG, "Rejected connection."); 284 | } else { 285 | Log.d(TAG, String.format("Reject unsuccessful. %d %s", 286 | status.getStatusCode(), status.getStatusMessage())); 287 | connection.setState(ConnectionState.AUTHENTICATING); 288 | } 289 | } 290 | }); 291 | } 292 | 293 | @Override 294 | protected void onNearbyConnectionRejected(String endpointId) { 295 | super.onNearbyConnectionRejected(endpointId); 296 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 297 | if (connection != null && connection.endpointMatches(endpointId)) { 298 | if (connection.isAutoConnect()) { 299 | // Avoid reconnecting. 300 | mAutoConnectEnabled = false; 301 | } 302 | clearRobocarConnection(); 303 | } 304 | } 305 | 306 | @Override 307 | protected void onNearbyConnected(String endpointId, ConnectionResolution connectionResolution) { 308 | super.onNearbyConnected(endpointId, connectionResolution); 309 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 310 | if (connection != null && connection.endpointMatches(endpointId)) { 311 | stopDiscovery(); 312 | connection.setState(ConnectionState.CONNECTED); 313 | savePairingInformation(connection); 314 | // We may have disabled this due to a canceled or rejected connection. Re-enable it now. 315 | mAutoConnectEnabled = true; 316 | } else { 317 | // This should never happen, but let's disconnect just to be safe. 318 | disconnectFromEndpoint(endpointId); 319 | } 320 | } 321 | 322 | @Override 323 | protected void onNearbyDisconnected(String endpointId) { 324 | super.onNearbyDisconnected(endpointId); 325 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 326 | if (connection != null && connection.endpointMatches(endpointId)) { 327 | clearRobocarConnection(); 328 | startDiscovery(); 329 | } 330 | } 331 | 332 | public void disconnect() { 333 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 334 | if (connection != null) { 335 | if (connection.isConnected()) { 336 | disconnectFromEndpoint(connection.getEndpointId()); 337 | // Avoid reconnecting. 338 | mAutoConnectEnabled = false; 339 | } 340 | // We don't receive onNearbyDisconnected() from the above, and we want to clear it 341 | // anyway to handle cancelation by the user. 342 | clearRobocarConnection(); 343 | // If we were connected and stopped discovering, this will start it again. 344 | startDiscovery(); 345 | } 346 | } 347 | 348 | private void clearRobocarConnection() { 349 | RobocarConnection connection = mRobocarConnectionLiveData.getValue(); 350 | if (connection != null) { 351 | connection.setState(ConnectionState.NOT_CONNECTED); 352 | mRobocarConnectionLiveData.setValue(null); 353 | } 354 | } 355 | 356 | private void clearEndpoints() { 357 | mEndpointMap.clear(); 358 | onEndpointsChanged(); 359 | } 360 | 361 | private void onEndpointsChanged() { 362 | mRobocarEndpointsLiveData.setValue(new ArrayList<>(mEndpointMap.values())); 363 | } 364 | 365 | private boolean isTheDroidWeAreLookingFor(AdvertisingInfo info) { 366 | return mPairedAdvertisingInfo != null && mPairedAdvertisingInfo.equals(info); 367 | } 368 | 369 | private void savePairingInformation(RobocarConnection connection) { 370 | AdvertisingInfo ai = connection.getAdvertisingInfo(); 371 | String authToken = connection.getAuthToken(); 372 | DiscovererInfo diWithToken = new DiscovererInfo(mDiscovererInfo.mCompanionId, authToken); 373 | AdvertisingInfo aiWithToken = new AdvertisingInfo(ai.mRobocarId, ai.mLedSequence, 374 | authToken); 375 | 376 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( 377 | mGoogleApiClient.getContext()); 378 | PreferenceUtils.saveAdvertisingInfo(prefs, aiWithToken); 379 | PreferenceUtils.saveDiscovererInfo(prefs, diWithToken); 380 | 381 | setDiscovererInfo(diWithToken); 382 | setPairedAdvertisingInfo(aiWithToken); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarDiscoveryFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.arch.lifecycle.Observer; 19 | import android.arch.lifecycle.ViewModelProviders; 20 | import android.os.Bundle; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.app.Fragment; 23 | import android.support.v7.widget.DividerItemDecoration; 24 | import android.support.v7.widget.RecyclerView; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | 29 | import com.example.androidthings.robocar.companion.CompanionViewModel.NavigationState; 30 | 31 | import java.util.List; 32 | 33 | 34 | public class RobocarDiscoveryFragment extends Fragment { 35 | 36 | private static final String FRAGMENT_TAG_AUTH_DIALOG = "fragment.robocar_auth_dialog"; 37 | 38 | private RecyclerView mRecyclerView; 39 | private View mEmptyView; 40 | private RobocarEndpointsAdapter mAdapter; 41 | 42 | private CompanionViewModel mViewModel; 43 | 44 | @Nullable 45 | @Override 46 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 47 | @Nullable Bundle savedInstanceState) { 48 | return inflater.inflate(R.layout.fragment_discoverer, container, false); 49 | } 50 | 51 | @Override 52 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 53 | super.onViewCreated(view, savedInstanceState); 54 | mRecyclerView = view.findViewById(android.R.id.list); 55 | mRecyclerView.addItemDecoration(new DividerItemDecoration( 56 | mRecyclerView.getContext(), DividerItemDecoration.VERTICAL)); 57 | mEmptyView = view.findViewById(android.R.id.empty); 58 | } 59 | 60 | @Override 61 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 62 | super.onActivityCreated(savedInstanceState); 63 | 64 | mViewModel = ViewModelProviders.of(getActivity()).get(CompanionViewModel.class); 65 | RobocarDiscoverer discoverer = mViewModel.getRobocarDiscoverer(); 66 | 67 | mAdapter = new RobocarEndpointsAdapter(discoverer); 68 | mRecyclerView.setAdapter(mAdapter); 69 | discoverer.getRobocarEndpointsLiveData().observe(this, 70 | new Observer>() { 71 | @Override 72 | public void onChanged(@Nullable List list) { 73 | updateList(list); 74 | } 75 | }); 76 | 77 | discoverer.getRobocarConnectionLiveData().observe(this, new Observer() { 78 | @Override 79 | public void onChanged(@Nullable RobocarConnection connection) { 80 | clearAuthDialog(); 81 | if (connection != null) { 82 | if (connection.isConnected()) { 83 | // Advance to controller UI 84 | mViewModel.navigateTo(NavigationState.CONTROLLER_UI); 85 | } else { 86 | showAuthDialog(); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | 93 | private void updateList(List list) { 94 | boolean empty = list == null || list.isEmpty(); 95 | mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE); 96 | mRecyclerView.setVisibility(empty ? View.GONE : View.VISIBLE); 97 | mAdapter.setItems(list); 98 | } 99 | 100 | private void clearAuthDialog() { 101 | Fragment f = getFragmentManager().findFragmentByTag(FRAGMENT_TAG_AUTH_DIALOG); 102 | if (f != null) { 103 | getFragmentManager().beginTransaction().remove(f).commit(); 104 | } 105 | } 106 | 107 | private void showAuthDialog() { 108 | RobocarConnectionDialog dialog = new RobocarConnectionDialog(); 109 | dialog.show(getFragmentManager(), FRAGMENT_TAG_AUTH_DIALOG); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 19 | 20 | /** 21 | * Immutable class representing a Robocar's Nearby endpoint information. 22 | */ 23 | public class RobocarEndpoint { 24 | 25 | public final String mEndpointId; 26 | public final AdvertisingInfo mAdvertisingInfo; 27 | public final boolean mIsPaired; 28 | public final boolean mIsRemembered; 29 | 30 | public RobocarEndpoint(String endpointId, AdvertisingInfo advertisingInfo, 31 | boolean isRemembered) { 32 | if (endpointId == null) { 33 | throw new IllegalArgumentException("Endpoint ID cannot be null"); 34 | } 35 | if (advertisingInfo == null) { 36 | throw new IllegalArgumentException("Advertising Info cannot be null"); 37 | } 38 | mEndpointId = endpointId; 39 | mAdvertisingInfo = advertisingInfo; 40 | mIsPaired = mAdvertisingInfo.mIsPaired; 41 | mIsRemembered = mIsPaired && isRemembered; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object obj) { 46 | if (this == obj) { 47 | return true; 48 | } 49 | if (obj == null || !obj.getClass().equals(RobocarEndpoint.class)) { 50 | return false; 51 | } 52 | return this.mEndpointId.equals(((RobocarEndpoint) obj).mEndpointId); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return mEndpointId.hashCode(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /companion/src/main/java/com/example/androidthings/robocar/companion/RobocarEndpointsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.androidthings.robocar.companion; 17 | 18 | import android.support.v7.widget.RecyclerView; 19 | import android.support.v7.widget.RecyclerView.ViewHolder; 20 | import android.view.LayoutInflater; 21 | import android.view.View; 22 | import android.view.View.OnClickListener; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import com.example.androidthings.robocar.shared.model.AdvertisingInfo; 27 | 28 | import java.util.List; 29 | 30 | public class RobocarEndpointsAdapter 31 | extends RecyclerView.Adapter { 32 | 33 | private List mList; 34 | private RobocarDiscoverer mRobocarDiscoverer; 35 | 36 | public RobocarEndpointsAdapter(RobocarDiscoverer robocarDiscoverer) { 37 | mRobocarDiscoverer = robocarDiscoverer; 38 | } 39 | 40 | @Override 41 | public int getItemCount() { 42 | return mList == null ? 0 : mList.size(); 43 | } 44 | 45 | @Override 46 | public RobocarEndpointViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 47 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 48 | View itemView = inflater.inflate(R.layout.list_item_robocar_endpoint, parent, false); 49 | return new RobocarEndpointViewHolder(itemView); 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(RobocarEndpointViewHolder holder, int position) { 54 | holder.bind(mList.get(position)); 55 | } 56 | 57 | public void setItems(List newList) { 58 | mList = newList; 59 | notifyDataSetChanged(); 60 | } 61 | 62 | class RobocarEndpointViewHolder extends ViewHolder implements OnClickListener { 63 | 64 | private final TextView mNameView; 65 | private final TextView mColorPatternView; 66 | 67 | private RobocarEndpoint mRobocarEndpoint; 68 | 69 | public RobocarEndpointViewHolder(View itemView) { 70 | super(itemView); 71 | mNameView = itemView.findViewById(R.id.name); 72 | mColorPatternView = itemView.findViewById(R.id.color_pattern); 73 | itemView.setOnClickListener(this); 74 | } 75 | 76 | public void bind(RobocarEndpoint item) { 77 | mRobocarEndpoint = item; 78 | mNameView.setText(mNameView.getResources() 79 | .getString(R.string.robocar_name, item.mAdvertisingInfo.mRobocarId)); 80 | mColorPatternView.setText( 81 | AdvertisingInfo.ledColorsToString(item.mAdvertisingInfo.mLedSequence)); 82 | 83 | boolean enabled = canConnect(); 84 | mNameView.setEnabled(enabled); 85 | mColorPatternView.setEnabled(enabled); 86 | } 87 | 88 | private boolean canConnect() { 89 | return !mRobocarEndpoint.mIsPaired || mRobocarEndpoint.mIsRemembered; 90 | } 91 | 92 | @Override 93 | public void onClick(View view) { 94 | if (canConnect()) { 95 | mRobocarDiscoverer.requestConnection(mRobocarEndpoint.mEndpointId); 96 | } else { 97 | // TODO show dialog explaining how to reset Robocar 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /companion/src/main/res/animator/button_state_list_anim_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 42 | 43 | 44 | 45 | 46 | 50 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /companion/src/main/res/color/car_control_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_down_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_stop_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_turn_left_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_turn_right_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /companion/src/main/res/drawable/ic_up_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /companion/src/main/res/layout-land/fragment_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | 26 | 29 | 42 | 43 | 54 | 55 | 66 | 67 | 79 | 80 | 92 | 93 | 103 | 104 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /companion/src/main/res/layout/activity_companion.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /companion/src/main/res/layout/fragment_auth_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 27 | 28 | 37 | 38 |