├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.txt └── src │ ├── iot │ └── AndroidManifest.xml │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── ANDROID-OPEN-SOURCE-PROJECT-LICENSE.txt │ ├── ANDROID-SOFTWARE-DEVELOPMENT-KIT.txt │ ├── APACHE-LICENSE-2.0.txt │ └── service-names-port-numbers.csv │ ├── java │ └── com │ │ └── druk │ │ └── servicebrowser │ │ ├── BonjourApplication.java │ │ ├── Config.java │ │ ├── FavouritesManager.java │ │ ├── RegTypeManager.java │ │ ├── RegistrationManager.java │ │ ├── Utils.java │ │ └── ui │ │ ├── HTMLViewerActivity.java │ │ ├── LicensesActivity.java │ │ ├── MainActivity.java │ │ ├── RegTypeActivity.java │ │ ├── RegisterServiceActivity.java │ │ ├── RegistrationsActivity.java │ │ ├── ServiceActivity.java │ │ ├── adapter │ │ ├── ServiceAdapter.java │ │ └── TxtRecordsAdapter.java │ │ ├── fragment │ │ ├── RegTypeBrowserFragment.java │ │ ├── ServiceBrowserFragment.java │ │ └── ServiceDetailFragment.java │ │ └── viewmodel │ │ ├── RegTypeBrowserViewModel.java │ │ ├── ServiceBrowserViewModel.java │ │ └── ServiceDetailViewModel.java │ └── res │ ├── drawable │ ├── ic_add.xml │ ├── ic_autorenew.xml │ ├── ic_check.xml │ ├── ic_open_in_browser.xml │ ├── ic_report_problem.xml │ ├── ic_settings_input_antenna.xml │ ├── ic_star.xml │ ├── ic_star_accent.xml │ ├── ic_star_border.xml │ └── ic_stop.xml │ ├── layout-sw600dp │ └── activity_main.xml │ ├── layout │ ├── activity_html_viewer.xml │ ├── activity_license.xml │ ├── activity_main.xml │ ├── activity_reg_type.xml │ ├── activity_service.xml │ ├── blank_activity.xml │ ├── dialog_add_txt_records.xml │ ├── fragment_register_service.xml │ ├── fragment_registrations.xml │ ├── fragment_service_browser.xml │ ├── fragment_service_detail.xml │ ├── one_text_item.xml │ └── two_text_item.xml │ ├── menu │ ├── menu_bonjour_browser.xml │ ├── menu_bonjour_browser_for_developers.xml │ ├── menu_domain.xml │ └── menu_registered_services.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-night-v23 │ └── styles.xml │ ├── values-night-v27 │ └── styles.xml │ ├── values-sw720dp │ └── dimens.xml │ ├── values-v23 │ └── styles.xml │ ├── values-v27 │ └── styles.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── art ├── Screenshot_1_framed.png ├── Screenshot_2_framed.png ├── Screenshot_2_framed_2.png ├── Screenshot_3_framed.png ├── Screenshot_4_framed.png ├── Screenshot_5_framed.png ├── Screenshots.pxm ├── feature_graphic.png ├── feature_graphic.pxm ├── res │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ │ └── ic_launcher.png └── web_hi_res_512.png ├── build.gradle ├── circle.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | obj/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | /*/build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Gradle 31 | .gradle/ 32 | build/ 33 | 34 | # IDEA files 35 | .idea/ 36 | out/ 37 | *.iml 38 | 39 | # Keys 40 | keys/ 41 | 42 | # Test reports 43 | /*.html 44 | 45 | fabric.properties 46 | app/release 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THE PROJECT IS ARCHIVED DUE TO THE RUSSIAN INVASION OF UKRAINE 2 | 3 | # 🇺🇦 #StandWithUkraine 4 | On Feb. 24, 2022 Russia declared an [unprovoked war on Ukraine](https://war.ukraine.ua/russia-war-crimes/) and launched a full-scale invasion. Russia is currently bombing peaceful Ukrainian cities, including schools and hospitals and attacking civilians who are fleeing conflict zones. 5 | 6 | Please support Ukraine by lobbying your governments, protesting peacefully, and donating money to support the people of Ukraine. Below are links to trustworthy organizations that are helping to defend Ukraine in this unprovoked war: 7 | 8 | * [Donate to Come Back Alive](https://www.comebackalive.in.ua/donate) 9 | * [Donate to KOLO](https://koloua.com/en/) 10 | * [Donate to Prytula Foundation](https://prytulafoundation.org/en) 11 | 12 | # Service Browser [![Circle CI](https://circleci.com/gh/andriydruk/BonjourBrowser.svg?style=shield&circle-token=8df2706f8f9aaa62b617722a3d90561781aa4c68)](https://circleci.com/gh/andriydruk/BonjourBrowser) 13 | 14 | Service Browser is a utility that discovers all services registered in a network and resolves meta-information about them, including IP addresses and port. 15 | It also provides an ability to register your own service in any domain (available only for developers). 16 | 17 | The source code is available under the Apache 2.0 license. 18 | 19 | License 20 | ------- 21 | 22 | Licensed under the Apache License, Version 2.0 (the "License"); 23 | you may not use this file except in compliance with the License. 24 | You may obtain a copy of the License at 25 | 26 | http://www.apache.org/licenses/LICENSE-2.0 27 | 28 | Unless required by applicable law or agreed to in writing, software 29 | distributed under the License is distributed on an "AS IS" BASIS, 30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | See the License for the specific language governing permissions and 32 | limitations under the License. 33 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 33 5 | 6 | defaultConfig { 7 | minSdkVersion 22 8 | targetSdkVersion 33 9 | versionCode 2_002_000 10 | versionName "2.2.0" 11 | 12 | vectorDrawables.useSupportLibrary = true 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 19 | } 20 | 21 | debug { 22 | debuggable true 23 | } 24 | 25 | /** 26 | * It's my IOT build type for running as builtin app on Android Things. 27 | * I use this type of build for testing on huge networks with my Raspberry Pi 3. 28 | */ 29 | iot { 30 | initWith debug 31 | debuggable true 32 | applicationIdSuffix ".iot" 33 | } 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility 1.8 38 | targetCompatibility 1.8 39 | } 40 | namespace 'com.druk.servicebrowser' 41 | } 42 | 43 | dependencies { 44 | implementation "com.github.andriydruk:rx2dnssd:0.9.17" 45 | 46 | implementation "androidx.annotation:annotation:1.5.0" 47 | implementation "androidx.appcompat:appcompat:1.5.1" 48 | implementation "androidx.recyclerview:recyclerview:1.2.1" 49 | implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" 50 | 51 | implementation "androidx.lifecycle:lifecycle-viewmodel:2.5.1" 52 | 53 | implementation "androidx.cardview:cardview:1.0.0" 54 | implementation "androidx.browser:browser:1.4.0" 55 | implementation "com.google.android.material:material:1.7.0" 56 | } 57 | -------------------------------------------------------------------------------- /app/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Developer/Android/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 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 | #} -------------------------------------------------------------------------------- /app/src/iot/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 21 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/assets/ANDROID-OPEN-SOURCE-PROJECT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2005-2008, 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 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | 13 | 14 | Apache License 15 | Version 2.0, January 2004 16 | http://www.apache.org/licenses/ 17 | 18 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 19 | 20 | 1. Definitions. 21 | 22 | "License" shall mean the terms and conditions for use, reproduction, 23 | and distribution as defined by Sections 1 through 9 of this document. 24 | 25 | "Licensor" shall mean the copyright owner or entity authorized by 26 | the copyright owner that is granting the License. 27 | 28 | "Legal Entity" shall mean the union of the acting entity and all 29 | other entities that control, are controlled by, or are under common 30 | control with that entity. For the purposes of this definition, 31 | "control" means (i) the power, direct or indirect, to cause the 32 | direction or management of such entity, whether by contract or 33 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 34 | outstanding shares, or (iii) beneficial ownership of such entity. 35 | 36 | "You" (or "Your") shall mean an individual or Legal Entity 37 | exercising permissions granted by this License. 38 | 39 | "Source" form shall mean the preferred form for making modifications, 40 | including but not limited to software source code, documentation 41 | source, and configuration files. 42 | 43 | "Object" form shall mean any form resulting from mechanical 44 | transformation or translation of a Source form, including but 45 | not limited to compiled object code, generated documentation, 46 | and conversions to other media types. 47 | 48 | "Work" shall mean the work of authorship, whether in Source or 49 | Object form, made available under the License, as indicated by a 50 | copyright notice that is included in or attached to the work 51 | (an example is provided in the Appendix below). 52 | 53 | "Derivative Works" shall mean any work, whether in Source or Object 54 | form, that is based on (or derived from) the Work and for which the 55 | editorial revisions, annotations, elaborations, or other modifications 56 | represent, as a whole, an original work of authorship. For the purposes 57 | of this License, Derivative Works shall not include works that remain 58 | separable from, or merely link (or bind by name) to the interfaces of, 59 | the Work and Derivative Works thereof. 60 | 61 | "Contribution" shall mean any work of authorship, including 62 | the original version of the Work and any modifications or additions 63 | to that Work or Derivative Works thereof, that is intentionally 64 | submitted to Licensor for inclusion in the Work by the copyright owner 65 | or by an individual or Legal Entity authorized to submit on behalf of 66 | the copyright owner. For the purposes of this definition, "submitted" 67 | means any form of electronic, verbal, or written communication sent 68 | to the Licensor or its representatives, including but not limited to 69 | communication on electronic mailing lists, source code control systems, 70 | and issue tracking systems that are managed by, or on behalf of, the 71 | Licensor for the purpose of discussing and improving the Work, but 72 | excluding communication that is conspicuously marked or otherwise 73 | designated in writing by the copyright owner as "Not a Contribution." 74 | 75 | "Contributor" shall mean Licensor and any individual or Legal Entity 76 | on behalf of whom a Contribution has been received by Licensor and 77 | subsequently incorporated within the Work. 78 | 79 | 2. Grant of Copyright License. Subject to the terms and conditions of 80 | this License, each Contributor hereby grants to You a perpetual, 81 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 82 | copyright license to reproduce, prepare Derivative Works of, 83 | publicly display, publicly perform, sublicense, and distribute the 84 | Work and such Derivative Works in Source or Object form. 85 | 86 | 3. Grant of Patent License. Subject to the terms and conditions of 87 | this License, each Contributor hereby grants to You a perpetual, 88 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 89 | (except as stated in this section) patent license to make, have made, 90 | use, offer to sell, sell, import, and otherwise transfer the Work, 91 | where such license applies only to those patent claims licensable 92 | by such Contributor that are necessarily infringed by their 93 | Contribution(s) alone or by combination of their Contribution(s) 94 | with the Work to which such Contribution(s) was submitted. If You 95 | institute patent litigation against any entity (including a 96 | cross-claim or counterclaim in a lawsuit) alleging that the Work 97 | or a Contribution incorporated within the Work constitutes direct 98 | or contributory patent infringement, then any patent licenses 99 | granted to You under this License for that Work shall terminate 100 | as of the date such litigation is filed. 101 | 102 | 4. Redistribution. You may reproduce and distribute copies of the 103 | Work or Derivative Works thereof in any medium, with or without 104 | modifications, and in Source or Object form, provided that You 105 | meet the following conditions: 106 | 107 | (a) You must give any other recipients of the Work or 108 | Derivative Works a copy of this License; and 109 | 110 | (b) You must cause any modified files to carry prominent notices 111 | stating that You changed the files; and 112 | 113 | (c) You must retain, in the Source form of any Derivative Works 114 | that You distribute, all copyright, patent, trademark, and 115 | attribution notices from the Source form of the Work, 116 | excluding those notices that do not pertain to any part of 117 | the Derivative Works; and 118 | 119 | (d) If the Work includes a "NOTICE" text file as part of its 120 | distribution, then any Derivative Works that You distribute must 121 | include a readable copy of the attribution notices contained 122 | within such NOTICE file, excluding those notices that do not 123 | pertain to any part of the Derivative Works, in at least one 124 | of the following places: within a NOTICE text file distributed 125 | as part of the Derivative Works; within the Source form or 126 | documentation, if provided along with the Derivative Works; or, 127 | within a display generated by the Derivative Works, if and 128 | wherever such third-party notices normally appear. The contents 129 | of the NOTICE file are for informational purposes only and 130 | do not modify the License. You may add Your own attribution 131 | notices within Derivative Works that You distribute, alongside 132 | or as an addendum to the NOTICE text from the Work, provided 133 | that such additional attribution notices cannot be construed 134 | as modifying the License. 135 | 136 | You may add Your own copyright statement to Your modifications and 137 | may provide additional or different license terms and conditions 138 | for use, reproduction, or distribution of Your modifications, or 139 | for any such Derivative Works as a whole, provided Your use, 140 | reproduction, and distribution of the Work otherwise complies with 141 | the conditions stated in this License. 142 | 143 | 5. Submission of Contributions. Unless You explicitly state otherwise, 144 | any Contribution intentionally submitted for inclusion in the Work 145 | by You to the Licensor shall be under the terms and conditions of 146 | this License, without any additional terms or conditions. 147 | Notwithstanding the above, nothing herein shall supersede or modify 148 | the terms of any separate license agreement you may have executed 149 | with Licensor regarding such Contributions. 150 | 151 | 6. Trademarks. This License does not grant permission to use the trade 152 | names, trademarks, service marks, or product names of the Licensor, 153 | except as required for reasonable and customary use in describing the 154 | origin of the Work and reproducing the content of the NOTICE file. 155 | 156 | 7. Disclaimer of Warranty. Unless required by applicable law or 157 | agreed to in writing, Licensor provides the Work (and each 158 | Contributor provides its Contributions) on an "AS IS" BASIS, 159 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 160 | implied, including, without limitation, any warranties or conditions 161 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 162 | PARTICULAR PURPOSE. You are solely responsible for determining the 163 | appropriateness of using or redistributing the Work and assume any 164 | risks associated with Your exercise of permissions under this License. 165 | 166 | 8. Limitation of Liability. In no event and under no legal theory, 167 | whether in tort (including negligence), contract, or otherwise, 168 | unless required by applicable law (such as deliberate and grossly 169 | negligent acts) or agreed to in writing, shall any Contributor be 170 | liable to You for damages, including any direct, indirect, special, 171 | incidental, or consequential damages of any character arising as a 172 | result of this License or out of the use or inability to use the 173 | Work (including but not limited to damages for loss of goodwill, 174 | work stoppage, computer failure or malfunction, or any and all 175 | other commercial damages or losses), even if such Contributor 176 | has been advised of the possibility of such damages. 177 | 178 | 9. Accepting Warranty or Additional Liability. While redistributing 179 | the Work or Derivative Works thereof, You may choose to offer, 180 | and charge a fee for, acceptance of support, warranty, indemnity, 181 | or other liability obligations and/or rights consistent with this 182 | License. However, in accepting such obligations, You may act only 183 | on Your own behalf and on Your sole responsibility, not on behalf 184 | of any other Contributor, and only if You agree to indemnify, 185 | defend, and hold each Contributor harmless for any liability 186 | incurred by, or claims asserted against, such Contributor by reason 187 | of your accepting any such warranty or additional liability. 188 | 189 | END OF TERMS AND CONDITIONS 190 | 191 | -------------------------------------------------------------------------------- /app/src/main/assets/APACHE-LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/BonjourApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser; 17 | 18 | import android.app.Application; 19 | import android.content.Context; 20 | import android.os.Build; 21 | import android.os.StrictMode; 22 | import android.util.Log; 23 | 24 | import androidx.annotation.NonNull; 25 | 26 | import com.github.druk.rx2dnssd.Rx2Dnssd; 27 | import com.github.druk.rx2dnssd.Rx2DnssdBindable; 28 | import com.github.druk.rx2dnssd.Rx2DnssdEmbedded; 29 | 30 | public class BonjourApplication extends Application { 31 | 32 | private static final String TAG = "BonjourApplication"; 33 | private Rx2Dnssd mRxDnssd; 34 | private RegistrationManager mRegistrationManager; 35 | private RegTypeManager mRegTypeManager; 36 | private FavouritesManager mFavouritesManager; 37 | 38 | @Override 39 | public void onCreate() { 40 | super.onCreate(); 41 | 42 | if (BuildConfig.DEBUG) { 43 | 44 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 45 | .detectDiskReads() 46 | .detectDiskWrites() 47 | .detectNetwork() // or .detectAll() for all detectable problems 48 | .penaltyLog() 49 | .build()); 50 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 51 | .detectLeakedSqlLiteObjects() 52 | .detectLeakedClosableObjects() 53 | .penaltyLog() 54 | .build()); 55 | } 56 | mRxDnssd = createDnssd(); 57 | mRegistrationManager = new RegistrationManager(); 58 | mRegTypeManager = new RegTypeManager(this); 59 | mFavouritesManager = new FavouritesManager(this); 60 | } 61 | 62 | public static BonjourApplication getApplication(@NonNull Context context){ 63 | return ((BonjourApplication)context.getApplicationContext()); 64 | } 65 | 66 | public static Rx2Dnssd getRxDnssd(@NonNull Context context){ 67 | return ((BonjourApplication)context.getApplicationContext()).mRxDnssd; 68 | } 69 | 70 | public static RegistrationManager getRegistrationManager(@NonNull Context context){ 71 | return ((BonjourApplication) context.getApplicationContext()).mRegistrationManager; 72 | } 73 | 74 | public static RegTypeManager getRegTypeManager(@NonNull Context context){ 75 | return ((BonjourApplication) context.getApplicationContext()).mRegTypeManager; 76 | } 77 | 78 | public static FavouritesManager getFavouritesManager(@NonNull Context context){ 79 | return ((BonjourApplication)context.getApplicationContext()).mFavouritesManager; 80 | } 81 | 82 | private Rx2Dnssd createDnssd() { 83 | // https://developer.android.com/about/versions/12/behavior-changes-12#mdnsresponder 84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 85 | Log.i(TAG, "Using embedded version of dns sd"); 86 | return new Rx2DnssdEmbedded(this); 87 | } 88 | else { 89 | Log.i(TAG, "Using bindable version of dns sd"); 90 | return new Rx2DnssdBindable(this); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/Config.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser; 17 | 18 | import java.util.regex.Pattern; 19 | 20 | public final class Config { 21 | 22 | private static final boolean VERBOSE = false; 23 | 24 | /* 25 | * @see {http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt} 26 | */ 27 | public static final String SERVICES_DOMAIN = "_services._dns-sd._udp"; 28 | 29 | public static final String EMPTY_DOMAIN = "."; 30 | public static final String LOCAL_DOMAIN = "local."; 31 | public static final String TCP_REG_TYPE_SUFFIX = "_tcp"; 32 | public static final String UDP_REG_TYPE_SUFFIX = "_udp"; 33 | 34 | public static final String REG_TYPE_SEPARATOR = Pattern.quote("."); 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/FavouritesManager.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.content.pm.ShortcutInfo; 7 | import android.content.pm.ShortcutManager; 8 | import android.os.Build; 9 | 10 | import androidx.core.content.pm.ShortcutInfoCompat; 11 | import androidx.core.graphics.drawable.IconCompat; 12 | 13 | import com.druk.servicebrowser.ui.RegTypeActivity; 14 | 15 | import java.util.HashSet; 16 | import java.util.LinkedList; 17 | import java.util.List; 18 | import java.util.Set; 19 | 20 | public class FavouritesManager { 21 | 22 | private final Context context; 23 | private final SharedPreferences sharedPreferences; 24 | private final RegTypeManager regTypeManager; 25 | private final Set favouriteRegTypes; 26 | 27 | FavouritesManager(Context context) { 28 | this.context = context; 29 | sharedPreferences = context.getSharedPreferences("DEFAULT", Context.MODE_PRIVATE); 30 | regTypeManager = BonjourApplication.getRegTypeManager(context); 31 | favouriteRegTypes = new HashSet<>(sharedPreferences.getAll().keySet()); 32 | } 33 | 34 | public boolean isFavourite(String regType) { 35 | return favouriteRegTypes.contains(regType); 36 | } 37 | 38 | public void addToFavourites(String regType) { 39 | boolean success = favouriteRegTypes.add(regType); 40 | if (success) { 41 | sharedPreferences.edit().putBoolean(regType, true).apply(); 42 | updateDynamicShortcuts(); 43 | } 44 | } 45 | 46 | public void removeFromFavourites(String regType) { 47 | boolean success = favouriteRegTypes.remove(regType); 48 | if (success) { 49 | sharedPreferences.edit().remove(regType).apply(); 50 | updateDynamicShortcuts(); 51 | } 52 | } 53 | 54 | void updateDynamicShortcuts() { 55 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { 56 | return; 57 | } 58 | 59 | ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class); 60 | if (shortcutManager == null) { 61 | return; 62 | } 63 | 64 | List shortcuts = new LinkedList<>(); 65 | 66 | for (String regType : favouriteRegTypes) { 67 | String fullNameRegType = regTypeManager.getRegTypeDescription(regType); 68 | if (fullNameRegType == null) { 69 | fullNameRegType = regType; 70 | } 71 | ShortcutInfoCompat newShortcut = new ShortcutInfoCompat.Builder(context, regType) 72 | .setShortLabel(fullNameRegType) 73 | .setLongLabel(fullNameRegType) 74 | .setIcon(IconCompat.createWithResource(context, R.drawable.ic_star_accent)) 75 | .setIntent(RegTypeActivity.createIntent(context, regType, Config.LOCAL_DOMAIN).setAction(Intent.ACTION_VIEW)) 76 | .build(); 77 | 78 | shortcuts.add(newShortcut.toShortcutInfo()); 79 | } 80 | 81 | shortcutManager.setDynamicShortcuts(shortcuts); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/RegTypeManager.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser; 2 | 3 | import android.content.Context; 4 | import androidx.annotation.NonNull; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.TreeMap; 15 | 16 | import io.reactivex.Flowable; 17 | import io.reactivex.schedulers.Schedulers; 18 | 19 | public class RegTypeManager { 20 | 21 | private static final String TAG = "RegTypeManager"; 22 | 23 | private TreeMap mServiceNamesTree; 24 | private Context mContext; 25 | 26 | RegTypeManager(@NonNull Context context) { 27 | this.mContext = context; 28 | // Load reg type descriptions as quick as possible on io thread 29 | Flowable.just("_zigbee-gateway._udp.") 30 | .map(this::getRegTypeDescription) 31 | .subscribeOn(Schedulers.io()) 32 | .observeOn(Schedulers.computation()) 33 | .subscribe(); 34 | } 35 | 36 | public List getListRegTypes() { 37 | if (this.mServiceNamesTree == null){ 38 | return new LinkedList<>(); 39 | } 40 | return new LinkedList<>(mServiceNamesTree.keySet()); 41 | } 42 | 43 | public String getRegTypeDescription(String regType) { 44 | if (mServiceNamesTree == null){ 45 | synchronized (this) { 46 | if (mServiceNamesTree == null) { 47 | mServiceNamesTree = new TreeMap<>(); 48 | try { 49 | InputStream is = mContext.getAssets().open("service-names-port-numbers.csv"); 50 | try { 51 | BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 52 | String line; 53 | while ((line = reader.readLine()) != null) { 54 | String[] rowData = line.split(","); 55 | if (rowData.length < 4 || TextUtils.isEmpty(rowData[0]) || TextUtils.isEmpty(rowData[2]) || TextUtils.isEmpty(rowData[3])) { 56 | continue; 57 | } 58 | if (rowData[0].contains(" ") || rowData[2].contains(" ")) { 59 | continue; 60 | } 61 | mServiceNamesTree.put("_" + rowData[0] + "._" + rowData[2] + ".", rowData[3]); 62 | } 63 | } catch (IOException ex) { 64 | // handle exception 65 | } finally { 66 | try { 67 | is.close(); 68 | } catch (IOException e) { 69 | Log.e(TAG, "init error: ", e); 70 | } 71 | } 72 | } catch (IOException e) { 73 | e.printStackTrace(); 74 | Log.e(TAG, "service-names-port-numbers.csv reading error: ", e); 75 | } 76 | } 77 | } 78 | } 79 | return mServiceNamesTree.get(regType); 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/RegistrationManager.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser; 2 | 3 | import android.content.Context; 4 | 5 | import com.github.druk.rx2dnssd.BonjourService; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | import io.reactivex.Observable; 14 | import io.reactivex.android.schedulers.AndroidSchedulers; 15 | import io.reactivex.disposables.Disposable; 16 | import io.reactivex.subjects.PublishSubject; 17 | 18 | 19 | public class RegistrationManager { 20 | 21 | private final Map mRegistrations = new HashMap<>(); 22 | 23 | public Observable register(Context context, BonjourService bonjourService) { 24 | PublishSubject subject = PublishSubject.create(); 25 | final Disposable[] subscriptions = new Disposable[1]; 26 | subscriptions[0] = BonjourApplication.getRxDnssd(context).register(bonjourService) 27 | .doOnNext(service -> mRegistrations.put(service, subscriptions[0])) 28 | .subscribeOn(AndroidSchedulers.mainThread()) 29 | .subscribe(subject::onNext); 30 | return subject; 31 | } 32 | 33 | public void unregister(BonjourService service) { 34 | Disposable subscription = mRegistrations.remove(service); 35 | subscription.dispose(); 36 | } 37 | 38 | public List getRegisteredServices() { 39 | return Collections.unmodifiableList(new ArrayList<>(mRegistrations.keySet())); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser; 18 | 19 | import java.text.SimpleDateFormat; 20 | import java.util.Calendar; 21 | import java.util.Date; 22 | import java.util.Locale; 23 | import java.util.TimeZone; 24 | 25 | public class Utils { 26 | 27 | private static final String TIME_FORMAT = "HH:mm:ss"; 28 | 29 | public static String formatTime(Long timestamp) { 30 | if (timestamp == null){ 31 | return ""; 32 | } 33 | Calendar cal = Calendar.getInstance(); 34 | TimeZone tz = cal.getTimeZone(); 35 | 36 | SimpleDateFormat sdf = new SimpleDateFormat(TIME_FORMAT, Locale.getDefault()); 37 | sdf.setTimeZone(tz); 38 | 39 | return sdf.format(new Date(timestamp)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/HTMLViewerActivity.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser.ui; 2 | 3 | /* 4 | * Copyright (C) 2008 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import com.druk.servicebrowser.R; 20 | 21 | import android.content.ActivityNotFoundException; 22 | import android.content.Intent; 23 | import android.os.Bundle; 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.appcompat.widget.Toolbar; 26 | import android.util.Log; 27 | import android.view.View; 28 | import android.webkit.WebChromeClient; 29 | import android.webkit.WebSettings; 30 | import android.webkit.WebView; 31 | import android.webkit.WebViewClient; 32 | import android.widget.Toast; 33 | 34 | import java.net.URISyntaxException; 35 | 36 | /** 37 | * Simple activity that shows the requested HTML page. This utility is 38 | * purposefully very limited in what it supports, including no network or 39 | * JavaScript. 40 | */ 41 | public class HTMLViewerActivity extends AppCompatActivity { 42 | private static final String TAG = "HTMLViewer"; 43 | 44 | private WebView mWebView; 45 | private View mLoading; 46 | 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | 51 | setContentView(R.layout.activity_html_viewer); 52 | 53 | Toolbar toolbar = findViewById(R.id.toolbar); 54 | setSupportActionBar(toolbar); 55 | if (getSupportActionBar() != null) { 56 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 57 | } 58 | 59 | mWebView = findViewById(R.id.webview); 60 | mLoading = findViewById(R.id.loading); 61 | 62 | mWebView.setWebChromeClient(new ChromeClient()); 63 | mWebView.setWebViewClient(new ViewClient()); 64 | 65 | WebSettings s = mWebView.getSettings(); 66 | s.setUseWideViewPort(true); 67 | s.setSupportZoom(true); 68 | s.setBuiltInZoomControls(true); 69 | s.setDisplayZoomControls(false); 70 | s.setSavePassword(false); 71 | s.setSaveFormData(false); 72 | s.setBlockNetworkLoads(true); 73 | 74 | // Javascript is purposely disabled, so that nothing can be 75 | // automatically run. 76 | s.setJavaScriptEnabled(false); 77 | s.setDefaultTextEncodingName("utf-8"); 78 | 79 | final Intent intent = getIntent(); 80 | if (intent.hasExtra(Intent.EXTRA_TITLE)) { 81 | setTitle(intent.getStringExtra(Intent.EXTRA_TITLE)); 82 | } 83 | 84 | mWebView.loadUrl(String.valueOf(intent.getData())); 85 | } 86 | 87 | @Override 88 | protected void onDestroy() { 89 | super.onDestroy(); 90 | mWebView.destroy(); 91 | } 92 | 93 | private class ChromeClient extends WebChromeClient { 94 | @Override 95 | public void onReceivedTitle(WebView view, String title) { 96 | if (!getIntent().hasExtra(Intent.EXTRA_TITLE)) { 97 | HTMLViewerActivity.this.setTitle(title); 98 | } 99 | } 100 | } 101 | 102 | private class ViewClient extends WebViewClient { 103 | @Override 104 | public void onPageFinished(WebView view, String url) { 105 | mLoading.setVisibility(View.GONE); 106 | } 107 | 108 | @Override 109 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 110 | Intent intent; 111 | // Perform generic parsing of the URI to turn it into an Intent. 112 | try { 113 | intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 114 | } catch (URISyntaxException ex) { 115 | Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage()); 116 | Toast.makeText(HTMLViewerActivity.this, R.string.cannot_open_link, Toast.LENGTH_SHORT).show(); 117 | return true; 118 | } 119 | // Sanitize the Intent, ensuring web pages can not bypass browser 120 | // security (only access to BROWSABLE activities). 121 | intent.addCategory(Intent.CATEGORY_BROWSABLE); 122 | intent.setComponent(null); 123 | Intent selector = intent.getSelector(); 124 | if (selector != null) { 125 | selector.addCategory(Intent.CATEGORY_BROWSABLE); 126 | selector.setComponent(null); 127 | } 128 | 129 | try { 130 | view.getContext().startActivity(intent); 131 | } catch (ActivityNotFoundException ex) { 132 | Log.w(TAG, "No application can handle " + url); 133 | Toast.makeText(HTMLViewerActivity.this, R.string.cannot_open_link, Toast.LENGTH_SHORT).show(); 134 | } 135 | return true; 136 | } 137 | } 138 | 139 | @Override 140 | public boolean onSupportNavigateUp() { 141 | onBackPressed(); 142 | return true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/LicensesActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import com.druk.servicebrowser.R; 19 | 20 | import android.content.ActivityNotFoundException; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.net.Uri; 24 | import android.os.Bundle; 25 | import androidx.appcompat.app.AppCompatActivity; 26 | import androidx.appcompat.widget.AppCompatTextView; 27 | import androidx.recyclerview.widget.LinearLayoutManager; 28 | import androidx.recyclerview.widget.RecyclerView; 29 | import androidx.appcompat.widget.Toolbar; 30 | import android.util.Log; 31 | import android.util.TypedValue; 32 | import android.view.LayoutInflater; 33 | import android.view.View; 34 | import android.view.ViewGroup; 35 | 36 | public class LicensesActivity extends AppCompatActivity implements View.OnClickListener { 37 | 38 | private static String[] LICENSE_SOFTWARE = new String[]{"Android Compatibility Library v4", "Android Compatibility Library v7", "Android Design Support Library", "Android SDK", "mDNSResponder", "RxAndroid"}; 39 | 40 | private static final String ANDROID_ASSETS_FILE_PATH = "file:///android_asset/"; 41 | private static final String ANDROID_OPEN_SOURCE_PROJECT_LICENSE = "ANDROID-OPEN-SOURCE-PROJECT-LICENSE.txt"; 42 | private static final String ANDROID_SOFTWARE_DEVELOPMENT_KIT = "ANDROID-SOFTWARE-DEVELOPMENT-KIT.txt"; 43 | private static final String APACHE_LICENSE = "APACHE-LICENSE-2.0.txt"; 44 | 45 | private LinearLayoutManager mLayoutManager; 46 | private OpenSourceComponentAdapter mAdapter; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_license); 52 | 53 | Toolbar toolbar = findViewById(R.id.toolbar); 54 | setSupportActionBar(toolbar); 55 | if (getSupportActionBar() != null) { 56 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 57 | } 58 | 59 | mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); 60 | mAdapter = new OpenSourceComponentAdapter(LICENSE_SOFTWARE, new String[]{ 61 | ANDROID_ASSETS_FILE_PATH + ANDROID_OPEN_SOURCE_PROJECT_LICENSE, 62 | ANDROID_ASSETS_FILE_PATH + ANDROID_OPEN_SOURCE_PROJECT_LICENSE, 63 | ANDROID_ASSETS_FILE_PATH + ANDROID_OPEN_SOURCE_PROJECT_LICENSE, 64 | ANDROID_ASSETS_FILE_PATH + ANDROID_SOFTWARE_DEVELOPMENT_KIT, 65 | ANDROID_ASSETS_FILE_PATH + APACHE_LICENSE, 66 | ANDROID_ASSETS_FILE_PATH + APACHE_LICENSE 67 | }); 68 | 69 | RecyclerView recyclerView = ((RecyclerView) findViewById(R.id.recycler_view)); 70 | recyclerView.setLayoutManager(mLayoutManager); 71 | recyclerView.setAdapter(mAdapter); 72 | } 73 | 74 | @Override 75 | protected void onResume() { 76 | super.onResume(); 77 | mAdapter.setListener(this); 78 | } 79 | 80 | @Override 81 | protected void onPause() { 82 | super.onPause(); 83 | mAdapter.setListener(null); 84 | } 85 | 86 | @Override 87 | public boolean onSupportNavigateUp() { 88 | onBackPressed(); 89 | return true; 90 | } 91 | 92 | @Override 93 | public void onClick(View v) { 94 | int position = mLayoutManager.getPosition(v); 95 | final Intent intent = new Intent(v.getContext(), HTMLViewerActivity.class); 96 | intent.setData(Uri.parse(mAdapter.getLicensePath(position))); 97 | intent.putExtra(Intent.EXTRA_TITLE, mAdapter.getComponentName(position)); 98 | intent.addCategory(Intent.CATEGORY_DEFAULT); 99 | 100 | try { 101 | v.getContext().startActivity(intent); 102 | } catch (ActivityNotFoundException e) { 103 | Log.e("TAG", "Failed to find viewer", e); 104 | } 105 | } 106 | 107 | private static class OpenSourceComponentAdapter extends RecyclerView.Adapter { 108 | 109 | private String[] componentNames; 110 | private String[] licensePaths; 111 | 112 | private View.OnClickListener listener; 113 | 114 | private OpenSourceComponentAdapter(String[] names, String[] paths) { 115 | this.componentNames = names; 116 | this.licensePaths = paths; 117 | } 118 | 119 | @Override 120 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { 121 | return new RecyclerView.ViewHolder(LayoutInflater.from(viewGroup.getContext()) 122 | .inflate(R.layout.one_text_item, viewGroup, false)) {}; 123 | } 124 | 125 | @Override 126 | public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) { 127 | ((AppCompatTextView) viewHolder.itemView).setText(componentNames[i]); 128 | viewHolder.itemView.setOnClickListener(listener); 129 | } 130 | 131 | @Override 132 | public int getItemCount() { 133 | return componentNames.length; 134 | } 135 | 136 | public void setListener(View.OnClickListener listener) { 137 | this.listener = listener; 138 | } 139 | 140 | public String getComponentName(int position) { 141 | return componentNames[position]; 142 | } 143 | 144 | public String getLicensePath(int position) { 145 | return licensePaths[position]; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import android.content.Intent; 19 | import android.os.Bundle; 20 | import androidx.fragment.app.Fragment; 21 | import androidx.slidingpanelayout.widget.SlidingPaneLayout; 22 | import androidx.appcompat.app.AppCompatActivity; 23 | import android.view.Menu; 24 | import android.view.MenuItem; 25 | import android.view.View; 26 | import android.widget.TextView; 27 | 28 | import com.druk.servicebrowser.Config; 29 | import com.druk.servicebrowser.R; 30 | import com.druk.servicebrowser.ui.fragment.RegTypeBrowserFragment; 31 | import com.druk.servicebrowser.ui.fragment.ServiceBrowserFragment; 32 | import com.druk.servicebrowser.ui.fragment.ServiceDetailFragment; 33 | import com.github.druk.rx2dnssd.BonjourService; 34 | 35 | import java.net.URL; 36 | 37 | public class MainActivity extends AppCompatActivity implements ServiceBrowserFragment.ServiceListener, ServiceDetailFragment.ServiceDetailListener { 38 | 39 | private static final String PARAM_DOMAIN = "param_domain"; 40 | private static final String PARAM_REG_TYPE = "param_reg_type"; 41 | private static final String PARAM_SERVICE_NAME = "param_service_name"; 42 | 43 | private SlidingPaneLayout slidingPanelLayout; 44 | private TextView noServiceTextView; 45 | private TextView serviceNameTextView; 46 | private TextView lastUpdatedTextView; 47 | 48 | private String domain; 49 | private String regType; 50 | private String serviceName; 51 | 52 | @Override 53 | protected void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | setContentView(R.layout.activity_main); 56 | setSupportActionBar(findViewById(R.id.toolbar)); 57 | slidingPanelLayout = findViewById(R.id.sliding_panel_layout); 58 | 59 | if (slidingPanelLayout != null) { 60 | slidingPanelLayout.openPane(); 61 | noServiceTextView = findViewById(R.id.no_service); 62 | serviceNameTextView = findViewById(R.id.service_name); 63 | lastUpdatedTextView = findViewById(R.id.last_updated); 64 | } 65 | 66 | if (savedInstanceState == null) { 67 | domain = Config.LOCAL_DOMAIN; 68 | getSupportFragmentManager().beginTransaction(). 69 | replace(R.id.first_panel, RegTypeBrowserFragment.newInstance(Config.TCP_REG_TYPE_SUFFIX)).commit(); 70 | } 71 | else{ 72 | domain = savedInstanceState.getString(PARAM_DOMAIN); 73 | regType = savedInstanceState.getString(PARAM_REG_TYPE); 74 | serviceName = savedInstanceState.getString(PARAM_SERVICE_NAME); 75 | } 76 | 77 | updateNavigation(); 78 | } 79 | 80 | private void updateNavigation(){ 81 | setTitle(domain + ((regType != null) ? " > " + regType + ((serviceName != null) ? " > " + serviceName : "") : "")); 82 | if (slidingPanelLayout != null){ 83 | noServiceTextView.setVisibility(serviceName == null ? View.VISIBLE : View.GONE); 84 | serviceNameTextView.setVisibility(serviceName == null ? View.GONE : View.VISIBLE); 85 | serviceNameTextView.setText(serviceName); 86 | lastUpdatedTextView.setVisibility(serviceName == null ? View.GONE : View.VISIBLE); 87 | } 88 | } 89 | 90 | @Override 91 | public boolean onCreateOptionsMenu(Menu menu) { 92 | getMenuInflater().inflate(R.menu.menu_bonjour_browser, menu); 93 | return true; 94 | } 95 | 96 | @Override 97 | public boolean onOptionsItemSelected(MenuItem item) { 98 | // Handle action bar item clicks here. The action bar will 99 | // automatically handle clicks on the Home/Up button, so long 100 | // as you specify a parent activity in AndroidManifest.xml. 101 | int id = item.getItemId(); 102 | 103 | //noinspection SimplifiableIfStatement 104 | if (id == R.id.action_license) { 105 | startActivity(new Intent(this, LicensesActivity.class)); 106 | return true; 107 | } 108 | else if (id == R.id.action_register) { 109 | RegistrationsActivity.startActivity(this); 110 | return true; 111 | } 112 | 113 | return super.onOptionsItemSelected(item); 114 | } 115 | 116 | @Override 117 | public void onServiceWasSelected(String domain, String regType, BonjourService service) { 118 | if (domain.equals(Config.EMPTY_DOMAIN)) { 119 | String[] regTypeParts = service.getRegType().split(Config.REG_TYPE_SEPARATOR); 120 | String serviceRegType = service.getServiceName() + "." + regTypeParts[0] + "."; 121 | String serviceDomain = regTypeParts[1] + "."; 122 | 123 | if (slidingPanelLayout != null) { 124 | getSupportFragmentManager().beginTransaction(). 125 | replace(R.id.second_panel, ServiceBrowserFragment.newInstance(serviceDomain, serviceRegType)).commit(); 126 | Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.third_panel); 127 | if (fragment != null) { 128 | getSupportFragmentManager().beginTransaction().remove(fragment).commit(); 129 | } 130 | this.regType = serviceRegType; 131 | this.serviceName = null; 132 | updateNavigation(); 133 | } 134 | else{ 135 | Intent intent = RegTypeActivity.createIntent(this, serviceRegType, serviceDomain); 136 | startActivity(intent); 137 | } 138 | } 139 | else{ 140 | ServiceDetailFragment fragment = ServiceDetailFragment.newInstance(service); 141 | getSupportFragmentManager().beginTransaction().replace(R.id.third_panel, fragment).commit(); 142 | slidingPanelLayout.closePane(); 143 | this.serviceName = service.getServiceName(); 144 | updateNavigation(); 145 | } 146 | } 147 | 148 | @Override 149 | protected void onSaveInstanceState(Bundle outState) { 150 | outState.putString(PARAM_DOMAIN, domain); 151 | outState.putString(PARAM_REG_TYPE, regType); 152 | outState.putString(PARAM_SERVICE_NAME, serviceName); 153 | super.onSaveInstanceState(outState); 154 | } 155 | 156 | @Override 157 | public void onServiceUpdated(BonjourService service) { 158 | lastUpdatedTextView.setText(getString(R.string.last_update, System.currentTimeMillis())); 159 | } 160 | 161 | @Override 162 | public void onServiceStopped(BonjourService service) { 163 | //Ignore this 164 | } 165 | 166 | @Override 167 | public void onHttpServerFound(URL url) { 168 | // TODO: show FAB 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/RegTypeActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import com.druk.servicebrowser.BonjourApplication; 19 | import com.druk.servicebrowser.R; 20 | import com.druk.servicebrowser.ui.fragment.ServiceBrowserFragment; 21 | import com.github.druk.rx2dnssd.BonjourService; 22 | 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.os.Bundle; 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.appcompat.widget.Toolbar; 28 | 29 | public class RegTypeActivity extends AppCompatActivity implements ServiceBrowserFragment.ServiceListener { 30 | 31 | private static final String KEY_REG_TYPE = "com.druk.servicebrowser.ui.RegTypeActivity.KEY_DOMAIN"; 32 | private static final String KEY_DOMAIN = "com.druk.servicebrowser.ui.RegTypeActivity.KEY_REG_TYPE"; 33 | 34 | public static Intent createIntent(Context context, String regType, String domain) { 35 | return new Intent(context, RegTypeActivity.class). 36 | putExtra(RegTypeActivity.KEY_DOMAIN, domain). 37 | putExtra(RegTypeActivity.KEY_REG_TYPE, regType); 38 | } 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_reg_type); 44 | 45 | Toolbar toolbar = findViewById(R.id.toolbar); 46 | setSupportActionBar(toolbar); 47 | if (getSupportActionBar() != null) { 48 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 49 | } 50 | 51 | 52 | if (getIntent() != null && getIntent().hasExtra(KEY_DOMAIN) && getIntent().hasExtra(KEY_REG_TYPE)) { 53 | String regType = getIntent().getStringExtra(KEY_REG_TYPE); 54 | String domain = getIntent().getStringExtra(KEY_DOMAIN); 55 | String description = BonjourApplication.getRegTypeManager(this).getRegTypeDescription(regType); 56 | if (description != null) { 57 | setTitle(description); 58 | } else { 59 | setTitle(regType); 60 | } 61 | if (savedInstanceState == null) { 62 | getSupportFragmentManager().beginTransaction(). 63 | replace(R.id.content, ServiceBrowserFragment.newInstance(domain, regType)).commit(); 64 | } 65 | } 66 | } 67 | 68 | @Override 69 | public boolean onSupportNavigateUp() { 70 | onBackPressed(); 71 | return true; 72 | } 73 | 74 | @Override 75 | public void onServiceWasSelected(String domain, String regType, BonjourService service) { 76 | ServiceActivity.startActivity(this, service); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/RegisterServiceActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import androidx.annotation.Nullable; 23 | import androidx.fragment.app.Fragment; 24 | import androidx.collection.ArrayMap; 25 | import androidx.appcompat.app.AlertDialog; 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.appcompat.widget.AppCompatAutoCompleteTextView; 28 | import androidx.recyclerview.widget.LinearLayoutManager; 29 | import androidx.recyclerview.widget.RecyclerView; 30 | import android.text.TextUtils; 31 | import android.view.KeyEvent; 32 | import android.view.LayoutInflater; 33 | import android.view.Menu; 34 | import android.view.MenuInflater; 35 | import android.view.MenuItem; 36 | import android.view.View; 37 | import android.view.ViewGroup; 38 | import android.view.inputmethod.EditorInfo; 39 | import android.view.inputmethod.InputMethodManager; 40 | import android.widget.ArrayAdapter; 41 | import android.widget.EditText; 42 | import android.widget.TextView; 43 | 44 | import com.druk.servicebrowser.BonjourApplication; 45 | import com.druk.servicebrowser.R; 46 | import com.druk.servicebrowser.ui.adapter.TxtRecordsAdapter; 47 | import com.github.druk.rx2dnssd.BonjourService; 48 | 49 | import java.util.List; 50 | 51 | public class RegisterServiceActivity extends AppCompatActivity { 52 | 53 | private static final String SERVICE = "service"; 54 | 55 | public static Intent createIntent(Context context) { 56 | return new Intent(context, RegisterServiceActivity.class); 57 | } 58 | 59 | public static BonjourService parseResult(Intent intent) { 60 | return intent.getParcelableExtra(SERVICE); 61 | } 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.blank_activity); 67 | setSupportActionBar(findViewById(R.id.toolbar)); 68 | if (getSupportActionBar() != null) { 69 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 70 | } 71 | 72 | if (savedInstanceState == null) { 73 | getSupportFragmentManager().beginTransaction().replace(R.id.content, new RegisterServiceFragment()).commit(); 74 | } 75 | } 76 | 77 | @Override 78 | public boolean onSupportNavigateUp() { 79 | onBackPressed(); 80 | return true; 81 | } 82 | 83 | private void setResult(BonjourService bonjourService) { 84 | setResult(Activity.RESULT_OK, new Intent().putExtra(SERVICE, bonjourService)); 85 | finish(); 86 | } 87 | 88 | public static class RegisterServiceFragment extends Fragment implements TextView.OnEditorActionListener, View.OnClickListener { 89 | 90 | private EditText serviceNameEditText; 91 | private AppCompatAutoCompleteTextView regTypeEditText; 92 | private EditText portEditText; 93 | private TxtRecordsAdapter adapter; 94 | private final ArrayMap mRecords = new ArrayMap<>(); 95 | 96 | @Override 97 | public void onAttach(Context context) { 98 | super.onAttach(context); 99 | setHasOptionsMenu(true); 100 | } 101 | 102 | @Nullable 103 | @Override 104 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 105 | View view = inflater.inflate(R.layout.fragment_register_service, container, false); 106 | serviceNameEditText = view.findViewById(R.id.service_name); 107 | regTypeEditText = view.findViewById(R.id.reg_type); 108 | portEditText = view.findViewById(R.id.port); 109 | 110 | serviceNameEditText.setOnEditorActionListener(this); 111 | regTypeEditText.setOnEditorActionListener(this); 112 | portEditText.setOnEditorActionListener(this); 113 | 114 | adapter = new TxtRecordsAdapter(){ 115 | 116 | @Override 117 | public void onItemClick(View view, int position) { 118 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 119 | final String key = getKey(position); 120 | String value = getValue(position); 121 | // Inflate and set the layout for the dialog 122 | // Pass null as the parent view because its going in the dialog layout 123 | builder.setMessage("Do you really want to delete " + key + "=" + value + " ?") 124 | .setPositiveButton(android.R.string.ok, (dialog, id1) -> { 125 | mRecords.remove(key); 126 | adapter.swapTXTRecords(mRecords); 127 | adapter.notifyDataSetChanged(); 128 | }) 129 | .setNegativeButton(android.R.string.cancel, (dialog, id1) -> { 130 | 131 | }); 132 | builder.create().show(); 133 | } 134 | }; 135 | 136 | RecyclerView recyclerView = view.findViewById(R.id.recycler_view); 137 | recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); 138 | recyclerView.setAdapter(adapter); 139 | 140 | List regTypes = BonjourApplication.getRegTypeManager(getContext()).getListRegTypes(); 141 | regTypeEditText.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.select_dialog_item, regTypes)); 142 | 143 | view.findViewById(R.id.fab).setOnClickListener(this); 144 | 145 | return view; 146 | } 147 | 148 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 149 | inflater.inflate(R.menu.menu_registered_services, menu); 150 | } 151 | 152 | @Override 153 | public boolean onOptionsItemSelected(MenuItem item) { 154 | // Handle action bar item clicks here. The action bar will 155 | // automatically handle clicks on the Home/Up button, so long 156 | // as you specify a parent activity in AndroidManifest.xml. 157 | int id = item.getItemId(); 158 | 159 | //noinspection SimplifiableIfStatement 160 | if (id == R.id.action_add) { 161 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 162 | View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_add_txt_records, null); 163 | final TextView keyTextView = view.findViewById(R.id.key); 164 | final TextView valueTextView = view.findViewById(R.id.value); 165 | // Inflate and set the layout for the dialog 166 | // Pass null as the parent view because its going in the dialog layout 167 | builder.setMessage("Add TXT record") 168 | .setView(view) 169 | .setPositiveButton(android.R.string.ok, (dialog, id1) -> { 170 | mRecords.put(keyTextView.getText().toString(), valueTextView.getText().toString()); 171 | adapter.swapTXTRecords(mRecords); 172 | adapter.notifyDataSetChanged(); 173 | }) 174 | .setNegativeButton(android.R.string.cancel, (dialog, id1) -> { 175 | 176 | }); 177 | builder.create().show(); 178 | return true; 179 | } 180 | 181 | return super.onOptionsItemSelected(item); 182 | } 183 | 184 | @Override 185 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 186 | if (getView() == null || actionId != EditorInfo.IME_ACTION_DONE) { 187 | return false; 188 | } 189 | switch (v.getId()) { 190 | case R.id.service_name: 191 | regTypeEditText.requestFocus(); 192 | return true; 193 | case R.id.reg_type: 194 | portEditText.requestFocus(); 195 | return true; 196 | case R.id.port: 197 | InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 198 | imm.hideSoftInputFromWindow(getView().getWindowToken(), 0); 199 | return true; 200 | } 201 | return false; 202 | } 203 | 204 | @Override 205 | public void onClick(View v) { 206 | if (getView() == null) { 207 | return; 208 | } 209 | String serviceName = serviceNameEditText.getText().toString(); 210 | String reqType = regTypeEditText.getText().toString(); 211 | String port = portEditText.getText().toString(); 212 | Integer portNumber = 0; 213 | 214 | boolean isValid = true; 215 | if (TextUtils.isEmpty(serviceName)) { 216 | isValid = false; 217 | serviceNameEditText.setError("Service name can't be unspecified"); 218 | } 219 | if (TextUtils.isEmpty(reqType)) { 220 | isValid = false; 221 | regTypeEditText.setError("Reg type can't be unspecified"); 222 | } 223 | if (TextUtils.isEmpty(port)) { 224 | isValid = false; 225 | portEditText.setError("Port can't be unspecified"); 226 | } else { 227 | try { 228 | portNumber = Integer.parseInt(port); 229 | if (portNumber < 0 || portNumber > 65535) { 230 | isValid = false; 231 | portEditText.setError("Invalid port number (0-65535)"); 232 | } 233 | } catch (NumberFormatException e) { 234 | isValid = false; 235 | portEditText.setError("Invalid port number (0-65535)"); 236 | } 237 | } 238 | 239 | if (isValid) { 240 | if (getActivity() instanceof RegisterServiceActivity) { 241 | ((RegisterServiceActivity) getActivity()).setResult( 242 | new BonjourService.Builder(0, 0, serviceName, reqType, null).port(portNumber).dnsRecords(mRecords).build()); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/RegistrationsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import androidx.annotation.Nullable; 23 | import androidx.fragment.app.Fragment; 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.recyclerview.widget.LinearLayoutManager; 26 | import androidx.recyclerview.widget.RecyclerView; 27 | import android.view.LayoutInflater; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.widget.Toast; 31 | 32 | import com.druk.servicebrowser.BonjourApplication; 33 | import com.druk.servicebrowser.R; 34 | import com.druk.servicebrowser.ui.adapter.ServiceAdapter; 35 | import com.github.druk.rx2dnssd.BonjourService; 36 | 37 | import java.util.List; 38 | 39 | import io.reactivex.android.schedulers.AndroidSchedulers; 40 | import io.reactivex.disposables.Disposable; 41 | 42 | public class RegistrationsActivity extends AppCompatActivity { 43 | 44 | public static void startActivity(Context context) { 45 | context.startActivity(new Intent(context, RegistrationsActivity.class)); 46 | } 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.blank_activity); 52 | setSupportActionBar(findViewById(R.id.toolbar)); 53 | if (getSupportActionBar() != null) { 54 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 55 | } 56 | 57 | if (savedInstanceState == null) { 58 | getSupportFragmentManager().beginTransaction().replace(R.id.content, new RegistrationsFragment()).commit(); 59 | } 60 | } 61 | 62 | @Override 63 | public boolean onSupportNavigateUp() { 64 | onBackPressed(); 65 | return true; 66 | } 67 | 68 | public static class RegistrationsFragment extends Fragment { 69 | 70 | private static final int REGISTER_REQUEST_CODE = 100; 71 | private static final int STOP_REQUEST_CODE = 101; 72 | 73 | private ServiceAdapter adapter; 74 | private Disposable mDisposable; 75 | private RecyclerView mRecyclerView; 76 | private View mNoServiceView; 77 | 78 | @Override 79 | public void onAttach(Context context) { 80 | super.onAttach(context); 81 | adapter = new ServiceAdapter(getContext()) { 82 | @Override 83 | public void onBindViewHolder(ViewHolder holder, final int position) { 84 | holder.text1.setText(getItem(position).getServiceName()); 85 | holder.text2.setText(getItem(position).getRegType()); 86 | holder.itemView.setOnClickListener(v -> startActivityForResult(ServiceActivity.startActivity(getContext(), getItem(position), true), STOP_REQUEST_CODE)); 87 | } 88 | }; 89 | } 90 | 91 | @Override 92 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 93 | if (requestCode == REGISTER_REQUEST_CODE) { 94 | if (resultCode == Activity.RESULT_OK) { 95 | BonjourService bonjourService = RegisterServiceActivity.parseResult(data); 96 | mDisposable = BonjourApplication.getRegistrationManager(getContext()) 97 | .register(getContext(), bonjourService) 98 | .observeOn(AndroidSchedulers.mainThread()) 99 | .subscribe(service -> RegistrationsFragment.this.updateServices(), throwable -> Toast.makeText(RegistrationsFragment.this.getContext(), "Error: " + throwable.getMessage(), Toast.LENGTH_SHORT).show()); 100 | } 101 | return; 102 | } 103 | else if (requestCode == STOP_REQUEST_CODE) { 104 | if (resultCode == Activity.RESULT_OK) { 105 | BonjourService bonjourService = ServiceActivity.parseResult(data); 106 | BonjourApplication.getRegistrationManager(getContext()).unregister(bonjourService); 107 | updateServices(); 108 | } 109 | return; 110 | } 111 | super.onActivityResult(requestCode, resultCode, data); 112 | } 113 | 114 | @Nullable 115 | @Override 116 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 117 | View view = inflater.inflate(R.layout.fragment_registrations, container, false); 118 | mRecyclerView = view.findViewById(R.id.recycler_view); 119 | mRecyclerView.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); 120 | mRecyclerView.setAdapter(adapter); 121 | mNoServiceView = view.findViewById(R.id.no_service); 122 | view.findViewById(R.id.fab).setOnClickListener(v -> RegistrationsFragment.this.startActivityForResult(RegisterServiceActivity.createIntent(getContext()), REGISTER_REQUEST_CODE)); 123 | updateServices(); 124 | return view; 125 | } 126 | 127 | @Override 128 | public void onStop() { 129 | super.onStop(); 130 | if (mDisposable != null && !mDisposable.isDisposed()) { 131 | mDisposable.dispose(); 132 | } 133 | } 134 | 135 | private void updateServices() { 136 | List registeredServices = BonjourApplication.getRegistrationManager(getContext()).getRegisteredServices(); 137 | adapter.swap(registeredServices); 138 | mNoServiceView.setVisibility(registeredServices.size() > 0 ? View.GONE : View.VISIBLE); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/ServiceActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.net.Uri; 22 | import android.os.Bundle; 23 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.browser.customtabs.CustomTabsIntent; 26 | import androidx.core.content.ContextCompat; 27 | 28 | import android.view.View; 29 | import android.view.animation.OvershootInterpolator; 30 | import android.widget.TextView; 31 | import android.widget.Toast; 32 | 33 | import com.druk.servicebrowser.R; 34 | import com.druk.servicebrowser.Utils; 35 | import com.druk.servicebrowser.ui.fragment.ServiceDetailFragment; 36 | import com.github.druk.rx2dnssd.BonjourService; 37 | 38 | import java.net.URL; 39 | 40 | public class ServiceActivity extends AppCompatActivity implements ServiceDetailFragment.ServiceDetailListener { 41 | 42 | private static final String SERVICE = "mService"; 43 | private static final String REGISTERED = "registered"; 44 | 45 | private TextView mServiceName; 46 | private TextView mRegType; 47 | private TextView mDomain; 48 | private TextView mLastTimestamp; 49 | 50 | public static void startActivity(Context context, BonjourService service) { 51 | context.startActivity(new Intent(context, ServiceActivity.class). 52 | putExtra(ServiceActivity.SERVICE, service)); 53 | } 54 | 55 | public static Intent startActivity(Context context, BonjourService service, boolean isRegistered) { 56 | return new Intent(context, ServiceActivity.class).putExtra(ServiceActivity.SERVICE, service).putExtra(REGISTERED, isRegistered); 57 | } 58 | 59 | public static BonjourService parseResult(Intent intent) { 60 | return intent.getParcelableExtra(SERVICE); 61 | } 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_service); 67 | setSupportActionBar(findViewById(R.id.toolbar)); 68 | if (getSupportActionBar() != null) { 69 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 70 | } 71 | 72 | FloatingActionButton fab = findViewById(R.id.fab); 73 | mServiceName = findViewById(R.id.service_name); 74 | mRegType = findViewById(R.id.reg_type); 75 | mDomain = findViewById(R.id.domain); 76 | mLastTimestamp = findViewById(R.id.last_timestamp); 77 | 78 | ServiceDetailFragment serviceDetailFragment; 79 | boolean isRegistered = getIntent().getBooleanExtra(REGISTERED, false); 80 | 81 | if (savedInstanceState == null){ 82 | BonjourService service = getIntent().getParcelableExtra(SERVICE); 83 | serviceDetailFragment = ServiceDetailFragment.newInstance(service); 84 | getSupportFragmentManager().beginTransaction().replace(R.id.content, serviceDetailFragment).commit(); 85 | } 86 | else { 87 | serviceDetailFragment = (ServiceDetailFragment) getSupportFragmentManager().findFragmentById(R.id.content); 88 | } 89 | 90 | if (isRegistered && fab != null) { 91 | fab.setVisibility(View.VISIBLE); 92 | fab.setOnClickListener(serviceDetailFragment); 93 | } 94 | } 95 | 96 | @Override 97 | public boolean onSupportNavigateUp() { 98 | onBackPressed(); 99 | return true; 100 | } 101 | 102 | @Override 103 | public void onServiceUpdated(BonjourService service) { 104 | mServiceName.setText(service.getServiceName()); 105 | mDomain.setText(getString(R.string.domain, service.getDomain())); 106 | mRegType.setText(getString(R.string.reg_type, service.getRegType())); 107 | mLastTimestamp.setText(getString(R.string.last_update, Utils.formatTime(System.currentTimeMillis()))); 108 | } 109 | 110 | @Override 111 | public void onServiceStopped(BonjourService service) { 112 | setResult(Activity.RESULT_OK, new Intent().putExtra(SERVICE, service)); 113 | finish(); 114 | } 115 | 116 | @Override 117 | public void onHttpServerFound(URL url) { 118 | FloatingActionButton fab = findViewById(R.id.fab); 119 | if (fab.getVisibility() != View.VISIBLE) { 120 | fab.setVisibility(View.VISIBLE); 121 | fab.setScaleX(0); 122 | fab.setScaleY(0); 123 | fab.animate() 124 | .alpha(1.0f) 125 | .scaleX(1) 126 | .scaleY(1) 127 | .setInterpolator(new OvershootInterpolator()) 128 | .start(); 129 | } 130 | 131 | fab.setOnClickListener(view -> { 132 | CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); 133 | CustomTabsIntent customTabsIntent = builder.build(); 134 | try { 135 | customTabsIntent.launchUrl(this, Uri.parse(url.toString())); 136 | } 137 | catch (Throwable e) { 138 | Toast.makeText(this, "Can't find browser", Toast.LENGTH_SHORT).show(); 139 | } 140 | }); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/adapter/ServiceAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui.adapter; 17 | 18 | import android.content.Context; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | import android.util.TypedValue; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import com.druk.servicebrowser.R; 27 | import com.github.druk.rx2dnssd.BonjourService; 28 | 29 | import java.util.ArrayList; 30 | import java.util.Collections; 31 | import java.util.List; 32 | 33 | public abstract class ServiceAdapter extends RecyclerView.Adapter { 34 | 35 | private final int mSelectedBackground; 36 | private final int mBackground; 37 | private final ArrayList services = new ArrayList<>(); 38 | 39 | private long mSelectedItemId = -1; 40 | 41 | protected ServiceAdapter(Context context) { 42 | TypedValue mTypedValue = new TypedValue(); 43 | context.getTheme().resolveAttribute(R.attr.colorSurface, mTypedValue, true); 44 | mBackground = mTypedValue.resourceId; 45 | 46 | context.getTheme().resolveAttribute(R.attr.colorPrimaryContainer, mTypedValue, true); 47 | mSelectedBackground = mTypedValue.resourceId; 48 | } 49 | 50 | @Override 51 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { 52 | return new ViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.two_text_item, viewGroup, false)); 53 | } 54 | 55 | @Override 56 | public int getItemCount() { 57 | return services.size(); 58 | } 59 | 60 | @Override 61 | public long getItemId(int position) { 62 | return services.get(position).hashCode(); 63 | } 64 | 65 | public BonjourService getItem(int position) { 66 | return services.get(position); 67 | } 68 | 69 | public void clear() { 70 | this.services.clear(); 71 | } 72 | 73 | public long getSelectedItemId() { 74 | return mSelectedItemId; 75 | } 76 | 77 | public void setSelectedItemId(long selectedPosition) { 78 | mSelectedItemId = selectedPosition; 79 | } 80 | 81 | protected int getBackground(int position){ 82 | return (getItemId(position) == mSelectedItemId) ? mSelectedBackground : mBackground; 83 | } 84 | 85 | public void add(BonjourService service) { 86 | this.services.remove(service); 87 | this.services.add(service); 88 | sortServices(services); 89 | } 90 | 91 | public void swap(List service) { 92 | this.services.clear(); 93 | this.services.addAll(service); 94 | sortServices(services); 95 | notifyDataSetChanged(); 96 | } 97 | 98 | public void remove(BonjourService bonjourService) { 99 | if (this.services.remove(bonjourService)) { 100 | sortServices(services); 101 | } 102 | } 103 | 104 | public void sortServices() { 105 | sortServices(services); 106 | } 107 | 108 | public void sortServices(ArrayList services) { 109 | Collections.sort(services, (lhs, rhs) -> lhs.getServiceName().compareTo(rhs.getServiceName())); 110 | } 111 | 112 | public static class ViewHolder extends RecyclerView.ViewHolder { 113 | public TextView text1; 114 | public TextView text2; 115 | 116 | ViewHolder(View itemView) { 117 | super(itemView); 118 | text1 = itemView.findViewById(R.id.text1); 119 | text2 = itemView.findViewById(R.id.text2); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/adapter/TxtRecordsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui.adapter; 17 | 18 | import android.content.ClipData; 19 | import android.content.ClipboardManager; 20 | import android.content.Context; 21 | import com.google.android.material.snackbar.Snackbar; 22 | import androidx.collection.ArrayMap; 23 | import androidx.collection.SimpleArrayMap; 24 | import androidx.recyclerview.widget.RecyclerView; 25 | 26 | import android.os.Build; 27 | import android.util.TypedValue; 28 | import android.view.LayoutInflater; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.TextView; 32 | 33 | import com.druk.servicebrowser.R; 34 | 35 | 36 | public class TxtRecordsAdapter extends RecyclerView.Adapter { 37 | 38 | private final SimpleArrayMap ipRecords = new SimpleArrayMap<>(); 39 | private final SimpleArrayMap txtRecords = new SimpleArrayMap<>(); 40 | 41 | public TxtRecordsAdapter() { 42 | } 43 | 44 | @Override 45 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { 46 | return new ViewHolder(LayoutInflater.from(viewGroup.getContext()) 47 | .inflate(R.layout.two_text_item, viewGroup, false)); 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(ViewHolder holder, final int position) { 52 | holder.text1.setText(getKey(position)); 53 | holder.text2.setText(getValue(position)); 54 | holder.itemView.setOnClickListener(v -> TxtRecordsAdapter.this.onItemClick(v, position)); 55 | } 56 | 57 | public void onItemClick(View view, int position){ 58 | Context context = view.getContext(); 59 | 60 | ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 61 | ClipData clip = ClipData.newPlainText(getKey(position), getValue(position)); 62 | clipboard.setPrimaryClip(clip); 63 | 64 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 65 | Snackbar snackbar = Snackbar.make(view, context.getResources().getString(R.string.copy_toast_message, getKey(position)), Snackbar.LENGTH_LONG); 66 | snackbar.show(); 67 | } 68 | } 69 | 70 | @Override 71 | public int getItemCount() { 72 | return ipRecords.size() + txtRecords.size(); 73 | } 74 | 75 | protected String getKey(int position) { 76 | if (position < ipRecords.size()) { 77 | return ipRecords.keyAt(position); 78 | } 79 | else { 80 | return txtRecords.keyAt(position - ipRecords.size()); 81 | } 82 | } 83 | 84 | protected String getValue(int position) { 85 | if (position < ipRecords.size()) { 86 | return ipRecords.valueAt(position); 87 | } 88 | else { 89 | return txtRecords.valueAt(position - ipRecords.size()); 90 | } 91 | } 92 | 93 | public void swapIPRecords(ArrayMap records) { 94 | this.ipRecords.clear(); 95 | this.ipRecords.putAll(records); 96 | } 97 | 98 | public void swapTXTRecords(ArrayMap records) { 99 | this.txtRecords.clear(); 100 | this.txtRecords.putAll(records); 101 | } 102 | 103 | public static class ViewHolder extends RecyclerView.ViewHolder { 104 | public TextView text1; 105 | public TextView text2; 106 | 107 | ViewHolder(View itemView) { 108 | super(itemView); 109 | text1 = itemView.findViewById(R.id.text1); 110 | text2 = itemView.findViewById(R.id.text2); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/fragment/RegTypeBrowserFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui.fragment; 17 | 18 | import android.graphics.drawable.Drawable; 19 | import android.os.Bundle; 20 | import android.util.Log; 21 | 22 | import androidx.core.content.ContextCompat; 23 | import androidx.fragment.app.Fragment; 24 | import androidx.lifecycle.ViewModelProvider; 25 | 26 | import com.druk.servicebrowser.BonjourApplication; 27 | import com.druk.servicebrowser.Config; 28 | import com.druk.servicebrowser.R; 29 | import com.druk.servicebrowser.RegTypeManager; 30 | import com.druk.servicebrowser.ui.adapter.ServiceAdapter; 31 | import com.druk.servicebrowser.ui.viewmodel.RegTypeBrowserViewModel; 32 | import com.github.druk.rx2dnssd.BonjourService; 33 | 34 | import java.util.ArrayList; 35 | import java.util.Collection; 36 | import java.util.Collections; 37 | 38 | import io.reactivex.functions.Consumer; 39 | 40 | import static com.druk.servicebrowser.Config.EMPTY_DOMAIN; 41 | 42 | 43 | public class RegTypeBrowserFragment extends ServiceBrowserFragment { 44 | 45 | private static final String TAG = "RegTypeBrowser"; 46 | 47 | private RegTypeManager mRegTypeManager; 48 | 49 | public static Fragment newInstance(String regType) { 50 | return fillArguments(new RegTypeBrowserFragment(), EMPTY_DOMAIN, regType); 51 | } 52 | 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | mRegTypeManager = BonjourApplication.getRegTypeManager(getContext()); 57 | mAdapter = new ServiceAdapter(getActivity()) { 58 | 59 | Drawable drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_star); 60 | 61 | @Override 62 | public void onBindViewHolder(ViewHolder viewHolder, int i) { 63 | RegTypeBrowserViewModel.BonjourDomain domain = (RegTypeBrowserViewModel.BonjourDomain) getItem(i); 64 | String regType = domain.getServiceName() + "." + domain.getRegType().split(Config.REG_TYPE_SEPARATOR)[0] + "."; 65 | String regTypeDescription = mRegTypeManager.getRegTypeDescription(regType); 66 | if (regTypeDescription != null) { 67 | viewHolder.text1.setText(regType + " (" + regTypeDescription + ")"); 68 | } else { 69 | viewHolder.text1.setText(regType); 70 | } 71 | 72 | if (favouritesManager.isFavourite(regType)) { 73 | viewHolder.text1.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); 74 | } 75 | else { 76 | viewHolder.text1.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 77 | } 78 | 79 | viewHolder.text2.setText(domain.serviceCount + " services"); 80 | viewHolder.itemView.setOnClickListener(mListener); 81 | } 82 | 83 | @Override 84 | public void sortServices(ArrayList services) { 85 | Collections.sort(services, (lhs, rhs) -> { 86 | String lhsRegType = lhs.getServiceName() + "." + lhs.getRegType().split(Config.REG_TYPE_SEPARATOR)[0] + "."; 87 | String rhsRegType = rhs.getServiceName() + "." + rhs.getRegType().split(Config.REG_TYPE_SEPARATOR)[0] + "."; 88 | boolean isLhsFavourite = favouritesManager.isFavourite(lhsRegType); 89 | boolean isRhsFavourite = favouritesManager.isFavourite(rhsRegType); 90 | if (isLhsFavourite && isRhsFavourite) { 91 | return lhs.getServiceName().compareTo(rhs.getServiceName()); 92 | } 93 | else if (isLhsFavourite) { 94 | return -1; 95 | } 96 | else if (isRhsFavourite) { 97 | return 1; 98 | } 99 | return lhs.getServiceName().compareTo(rhs.getServiceName()); 100 | }); 101 | } 102 | }; 103 | } 104 | 105 | @Override 106 | protected void createViewModel() { 107 | RegTypeBrowserViewModel viewModel = new ViewModelProvider(this) 108 | .get(RegTypeBrowserViewModel.class); 109 | 110 | final Consumer errorAction = throwable -> { 111 | Log.e("DNSSD", "Error: ", throwable); 112 | RegTypeBrowserFragment.this.showError(throwable); 113 | }; 114 | 115 | final Consumer> servicesAction = services -> { 116 | mAdapter.clear(); 117 | for (RegTypeBrowserViewModel.BonjourDomain bonjourDomain: services) { 118 | if (bonjourDomain.serviceCount > 0) { 119 | mAdapter.add(bonjourDomain); 120 | } 121 | } 122 | RegTypeBrowserFragment.this.showList(); 123 | mAdapter.notifyDataSetChanged(); 124 | }; 125 | 126 | viewModel.startDiscovery(servicesAction, errorAction); 127 | } 128 | 129 | @Override 130 | protected boolean favouriteMenuSupport() { 131 | return false; 132 | } 133 | 134 | @Override 135 | public void onStart() { 136 | super.onStart(); 137 | // Favourites can be changed 138 | mAdapter.sortServices(); 139 | mAdapter.notifyDataSetChanged(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/fragment/ServiceBrowserFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Andriy Druk 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.druk.servicebrowser.ui.fragment; 17 | 18 | import android.animation.Animator; 19 | import android.animation.AnimatorListenerAdapter; 20 | import android.content.Context; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.LayoutInflater; 24 | import android.view.Menu; 25 | import android.view.MenuInflater; 26 | import android.view.MenuItem; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | import android.view.animation.AccelerateDecelerateInterpolator; 30 | import android.widget.FrameLayout; 31 | import android.widget.LinearLayout; 32 | import android.widget.Toast; 33 | 34 | import androidx.annotation.NonNull; 35 | import androidx.annotation.Nullable; 36 | import androidx.fragment.app.Fragment; 37 | import androidx.lifecycle.ViewModelProvider; 38 | import androidx.recyclerview.widget.LinearLayoutManager; 39 | import androidx.recyclerview.widget.RecyclerView; 40 | 41 | import com.druk.servicebrowser.BonjourApplication; 42 | import com.druk.servicebrowser.BuildConfig; 43 | import com.druk.servicebrowser.FavouritesManager; 44 | import com.druk.servicebrowser.R; 45 | import com.druk.servicebrowser.ui.adapter.ServiceAdapter; 46 | import com.druk.servicebrowser.ui.viewmodel.ServiceBrowserViewModel; 47 | import com.github.druk.rx2dnssd.BonjourService; 48 | 49 | public class ServiceBrowserFragment extends Fragment { 50 | 51 | private static final String KEY_REG_TYPE = "reg_type"; 52 | private static final String KEY_DOMAIN = "domain"; 53 | private static final String KEY_SELECTED_POSITION = "selected_position"; 54 | 55 | protected FavouritesManager favouritesManager; 56 | 57 | protected ServiceAdapter mAdapter; 58 | protected String mReqType; 59 | protected String mDomain; 60 | protected RecyclerView mRecyclerView; 61 | protected LinearLayout mProgressView; 62 | protected LinearLayout mErrorView; 63 | 64 | protected View.OnClickListener mListener = new View.OnClickListener() { 65 | @Override 66 | public void onClick(View v) { 67 | int position = mRecyclerView.getLayoutManager().getPosition(v); 68 | mAdapter.setSelectedItemId(mAdapter.getItemId(position)); 69 | mAdapter.notifyDataSetChanged(); 70 | if (ServiceBrowserFragment.this.isAdded()) { 71 | BonjourService service = mAdapter.getItem(position); 72 | ((ServiceListener) ServiceBrowserFragment.this.getActivity()).onServiceWasSelected(mDomain, mReqType, service); 73 | } 74 | } 75 | }; 76 | 77 | public static Fragment newInstance(String domain, String regType) { 78 | return fillArguments(new ServiceBrowserFragment(), domain, regType); 79 | } 80 | 81 | protected static Fragment fillArguments(Fragment fragment, String domain, String regType) { 82 | Bundle bundle = new Bundle(); 83 | bundle.putString(KEY_DOMAIN, domain); 84 | bundle.putString(KEY_REG_TYPE, regType); 85 | fragment.setArguments(bundle); 86 | return fragment; 87 | } 88 | 89 | @Override 90 | public void onAttach(@NonNull Context context) { 91 | super.onAttach(context); 92 | 93 | if (!(context instanceof ServiceListener)) { 94 | throw new IllegalArgumentException("Fragment context should implement ServiceListener interface"); 95 | } 96 | 97 | favouritesManager = BonjourApplication.getFavouritesManager(context); 98 | } 99 | 100 | @Override 101 | public void onCreate(final Bundle savedInstanceState) { 102 | super.onCreate(savedInstanceState); 103 | 104 | if (getArguments() != null) { 105 | mReqType = getArguments().getString(KEY_REG_TYPE); 106 | mDomain = getArguments().getString(KEY_DOMAIN); 107 | } 108 | 109 | mAdapter = new ServiceAdapter(getActivity()) { 110 | @Override 111 | public void onBindViewHolder(ViewHolder viewHolder, int i) { 112 | BonjourService service = getItem(i); 113 | viewHolder.text1.setText(service.getServiceName()); 114 | if (service.getInet4Address() != null) { 115 | viewHolder.text2.setText(service.getInet4Address().getHostAddress()); 116 | } 117 | else if (service.getInet6Address() != null) { 118 | viewHolder.text2.setText(service.getInet6Address().getHostAddress()); 119 | } 120 | else { 121 | viewHolder.text2.setText(service.getHostname()); 122 | } 123 | viewHolder.itemView.setOnClickListener(mListener); 124 | viewHolder.itemView.setBackgroundResource(getBackground(i)); 125 | } 126 | }; 127 | 128 | createViewModel(); 129 | setHasOptionsMenu(favouriteMenuSupport()); 130 | } 131 | 132 | @Override 133 | public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { 134 | super.onCreateOptionsMenu(menu, inflater); 135 | inflater.inflate(R.menu.menu_domain, menu); 136 | MenuItem item = menu.findItem(R.id.action_star); 137 | boolean isFavourite = favouritesManager.isFavourite(mReqType); 138 | item.setChecked(isFavourite); 139 | item.setIcon(isFavourite ? R.drawable.ic_star : R.drawable.ic_star_border); 140 | } 141 | 142 | @Override 143 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 144 | if (item.getItemId() == R.id.action_star) { 145 | if (!item.isChecked()) { 146 | favouritesManager.addToFavourites(mReqType); 147 | item.setChecked(true); 148 | item.setIcon(R.drawable.ic_star); 149 | Toast.makeText(getContext(), mReqType + " saved to Favourites", Toast.LENGTH_LONG).show(); 150 | } 151 | else { 152 | favouritesManager.removeFromFavourites(mReqType); 153 | item.setChecked(false); 154 | item.setIcon(R.drawable.ic_star_border); 155 | Toast.makeText(getContext(), mReqType + " removed from Favourites", Toast.LENGTH_LONG).show(); 156 | } 157 | return true; 158 | } 159 | return super.onOptionsItemSelected(item); 160 | } 161 | 162 | protected boolean favouriteMenuSupport() { 163 | return true; 164 | } 165 | 166 | protected void createViewModel() { 167 | ServiceBrowserViewModel viewModel = new ViewModelProvider(this).get(ServiceBrowserViewModel.class); 168 | viewModel.startDiscovery(mReqType, mDomain, service -> { 169 | if (!service.isLost()) { 170 | mAdapter.add(service); 171 | } else { 172 | mAdapter.remove(service); 173 | } 174 | ServiceBrowserFragment.this.showList(); 175 | mAdapter.notifyDataSetChanged(); 176 | }, throwable -> { 177 | Log.e("DNSSD", "Error: ", throwable); 178 | ServiceBrowserFragment.this.showError(throwable); 179 | }); 180 | } 181 | 182 | @Nullable 183 | @Override 184 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 185 | FrameLayout rootView = (FrameLayout) inflater.inflate(R.layout.fragment_service_browser, container, false); 186 | mRecyclerView = rootView.findViewById(R.id.recycler_view); 187 | mProgressView = rootView.findViewById(R.id.progress); 188 | mErrorView = rootView.findViewById(R.id.error_container); 189 | mRecyclerView.setLayoutManager(new LinearLayoutManager(mRecyclerView.getContext())); 190 | mRecyclerView.setHasFixedSize(true); 191 | mRecyclerView.setAdapter(mAdapter); 192 | if (savedInstanceState != null) { 193 | mAdapter.setSelectedItemId(savedInstanceState.getLong(KEY_SELECTED_POSITION, -1L)); 194 | } 195 | return rootView; 196 | } 197 | 198 | @Override 199 | public void onSaveInstanceState(Bundle outState) { 200 | super.onSaveInstanceState(outState); 201 | outState.putLong(KEY_SELECTED_POSITION, mAdapter.getSelectedItemId()); 202 | } 203 | 204 | protected void showList(){ 205 | if (mAdapter.getItemCount() > 0) { 206 | mRecyclerView.setVisibility(View.VISIBLE); 207 | mProgressView.setVisibility(View.GONE); 208 | } 209 | else { 210 | mRecyclerView.setVisibility(View.GONE); 211 | mProgressView.setVisibility(View.VISIBLE); 212 | } 213 | } 214 | 215 | protected void showError(final Throwable e){ 216 | if (BuildConfig.BUILD_TYPE.equals("iot")) { 217 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); 218 | return; 219 | } 220 | getActivity().runOnUiThread(() -> { 221 | mRecyclerView.animate().alpha(0.0f).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new AnimatorListenerAdapter() { 222 | @Override 223 | public void onAnimationEnd(Animator animation) { 224 | mRecyclerView.setVisibility(View.GONE); 225 | } 226 | }).start(); 227 | mProgressView.animate().alpha(0.0f).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new AnimatorListenerAdapter() { 228 | @Override 229 | public void onAnimationEnd(Animator animation) { 230 | mProgressView.setVisibility(View.GONE); 231 | } 232 | }).start(); 233 | mErrorView.setAlpha(0.0f); 234 | mErrorView.setVisibility(View.VISIBLE); 235 | mErrorView.animate().alpha(1.0f).setInterpolator(new AccelerateDecelerateInterpolator()).start(); 236 | mErrorView.findViewById(R.id.send_report).setOnClickListener(v -> Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e)); 237 | }); 238 | } 239 | 240 | public interface ServiceListener { 241 | void onServiceWasSelected(String domain, String regType, BonjourService service); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/fragment/ServiceDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser.ui.fragment; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.Nullable; 10 | import androidx.collection.ArrayMap; 11 | import androidx.fragment.app.Fragment; 12 | import androidx.lifecycle.ViewModelProvider; 13 | import androidx.recyclerview.widget.LinearLayoutManager; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import com.druk.servicebrowser.BonjourApplication; 17 | import com.druk.servicebrowser.R; 18 | import com.druk.servicebrowser.ui.adapter.TxtRecordsAdapter; 19 | import com.druk.servicebrowser.ui.viewmodel.ServiceDetailViewModel; 20 | import com.github.druk.rx2dnssd.BonjourService; 21 | 22 | import java.net.Inet4Address; 23 | import java.net.InetAddress; 24 | import java.net.URL; 25 | 26 | public class ServiceDetailFragment extends Fragment implements View.OnClickListener { 27 | 28 | private static final String KEY_SERVICE = "com.druk.servicebrowser.ui.fragment.ServiceDetailFragment.key_service"; 29 | 30 | private BonjourService mService; 31 | private ServiceDetailViewModel viewModel; 32 | 33 | private TxtRecordsAdapter mAdapter; 34 | 35 | public static ServiceDetailFragment newInstance(BonjourService service){ 36 | ServiceDetailFragment fragment = new ServiceDetailFragment(); 37 | Bundle bundle = new Bundle(); 38 | bundle.putParcelable(KEY_SERVICE, service); 39 | fragment.setArguments(bundle); 40 | return fragment; 41 | } 42 | 43 | @Override 44 | public void onAttach(Context context) { 45 | super.onAttach(context); 46 | 47 | if (!(context instanceof ServiceDetailListener)) { 48 | throw new IllegalArgumentException("Fragment context should implement ServiceDetailListener interface"); 49 | } 50 | } 51 | 52 | @Override 53 | public void onCreate(final Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | if (getArguments() != null) { 56 | mService = getArguments().getParcelable(KEY_SERVICE); 57 | } 58 | mAdapter = new TxtRecordsAdapter(); 59 | 60 | viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class); 61 | viewModel.resolveIPRecords(mService, this::updateIPRecords); 62 | viewModel.resolveTXTRecords(mService, this::updateTXTRecords); 63 | } 64 | 65 | @Nullable 66 | @Override 67 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 68 | RecyclerView mRecyclerView = (RecyclerView) inflater.inflate(R.layout.fragment_service_detail, container, false); 69 | mRecyclerView.setLayoutManager(new LinearLayoutManager(mRecyclerView.getContext())); 70 | mRecyclerView.setHasFixedSize(true); 71 | mRecyclerView.setAdapter(mAdapter); 72 | updateIPRecords(mService); 73 | updateTXTRecords(mService); 74 | ((ServiceDetailListener)getActivity()).onServiceUpdated(mService); 75 | return mRecyclerView; 76 | } 77 | 78 | private void updateIPRecords(BonjourService service) { 79 | ArrayMap metaInfo = new ArrayMap<>(); 80 | for (InetAddress inetAddress : service.getInetAddresses()) { 81 | if (inetAddress instanceof Inet4Address) { 82 | metaInfo.put("Address IPv4", service.getInet4Address().getHostAddress() + ":" + service.getPort()); 83 | } 84 | else { 85 | metaInfo.put("Address IPv6", service.getInet6Address().getHostAddress() + ":" + service.getPort()); 86 | } 87 | } 88 | mAdapter.swapIPRecords(metaInfo); 89 | mAdapter.notifyDataSetChanged(); 90 | if (isAdded()) { 91 | ((ServiceDetailListener)getActivity()).onServiceUpdated(mService); 92 | } 93 | 94 | viewModel.checkHttpConnection(mService, this::onHttpServerFound); 95 | } 96 | 97 | private void updateTXTRecords(BonjourService service) { 98 | ArrayMap metaInfo = new ArrayMap<>(); 99 | metaInfo.putAll(service.getTxtRecords()); 100 | mAdapter.swapTXTRecords(metaInfo); 101 | mAdapter.notifyDataSetChanged(); 102 | if (isAdded()) { 103 | ((ServiceDetailListener)getActivity()).onServiceUpdated(mService); 104 | } 105 | } 106 | 107 | private void onHttpServerFound(URL url) { 108 | ((ServiceDetailListener)getActivity()).onHttpServerFound(url); 109 | } 110 | 111 | @Override 112 | public void onClick(View v) { 113 | ((ServiceDetailListener)getActivity()).onServiceStopped(mService); 114 | } 115 | 116 | public interface ServiceDetailListener{ 117 | void onServiceUpdated(BonjourService service); 118 | void onHttpServerFound(URL url); 119 | void onServiceStopped(BonjourService service); 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/viewmodel/RegTypeBrowserViewModel.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser.ui.viewmodel; 2 | 3 | import static com.druk.servicebrowser.Config.EMPTY_DOMAIN; 4 | import static com.druk.servicebrowser.Config.TCP_REG_TYPE_SUFFIX; 5 | import static com.druk.servicebrowser.Config.UDP_REG_TYPE_SUFFIX; 6 | 7 | import android.app.Application; 8 | import android.util.Log; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.lifecycle.AndroidViewModel; 12 | 13 | import com.druk.servicebrowser.BonjourApplication; 14 | import com.druk.servicebrowser.Config; 15 | import com.github.druk.rx2dnssd.BonjourService; 16 | import com.github.druk.rx2dnssd.Rx2Dnssd; 17 | 18 | import java.util.Collection; 19 | import java.util.HashMap; 20 | 21 | import io.reactivex.android.schedulers.AndroidSchedulers; 22 | import io.reactivex.disposables.Disposable; 23 | import io.reactivex.functions.Consumer; 24 | import io.reactivex.schedulers.Schedulers; 25 | 26 | public class RegTypeBrowserViewModel extends AndroidViewModel { 27 | 28 | private final HashMap mBrowsers = new HashMap<>(); 29 | private final HashMap mServices = new HashMap<>(); 30 | 31 | protected Rx2Dnssd mRxDnssd; 32 | protected Disposable mDisposable; 33 | 34 | public RegTypeBrowserViewModel(@NonNull Application application) { 35 | super(application); 36 | mRxDnssd = BonjourApplication.getRxDnssd(application); 37 | } 38 | 39 | @Override 40 | protected void onCleared() { 41 | super.onCleared(); 42 | if (mDisposable != null) { 43 | mDisposable.dispose(); 44 | } 45 | mServices.clear(); 46 | synchronized (this) { 47 | for (Disposable subscription : mBrowsers.values()) { 48 | subscription.dispose(); 49 | } 50 | mBrowsers.clear(); 51 | } 52 | } 53 | 54 | public HashMap getServices() { 55 | return mServices; 56 | } 57 | 58 | public void startDiscovery(Consumer> servicesAction, Consumer errorAction) { 59 | final Consumer serviceAction = service -> { 60 | String[] regTypeParts = service.getRegType().split(Config.REG_TYPE_SEPARATOR); 61 | String serviceRegType = regTypeParts[0]; 62 | String protocolSuffix = regTypeParts[1]; 63 | String key = RegTypeBrowserViewModel.createKey(EMPTY_DOMAIN, protocolSuffix + "." + service.getDomain(), serviceRegType); 64 | RegTypeBrowserViewModel.BonjourDomain domain = mServices.get(key); 65 | if (domain != null) { 66 | if (service.isLost()) { 67 | domain.serviceCount--; 68 | } else { 69 | domain.serviceCount++; 70 | } 71 | servicesAction.accept(mServices.values()); 72 | } else { 73 | Log.w("TAG", "Service from unknown service type " + key); 74 | } 75 | }; 76 | 77 | Consumer reqTypeAction = new Consumer() { 78 | @Override 79 | public void accept(BonjourService service) { 80 | if (service.isLost()) { 81 | //Ignore this call 82 | return; 83 | } 84 | String[] regTypeParts = service.getRegType().split(Config.REG_TYPE_SEPARATOR); 85 | String protocolSuffix = regTypeParts[0]; 86 | String serviceDomain = regTypeParts[1]; 87 | if (TCP_REG_TYPE_SUFFIX.equals(protocolSuffix) || UDP_REG_TYPE_SUFFIX.equals(protocolSuffix)) { 88 | String key = service.getServiceName() + "." + protocolSuffix; 89 | synchronized (this) { 90 | if (!mBrowsers.containsKey(key)) { 91 | mBrowsers.put(key, mRxDnssd.browse(key, serviceDomain) 92 | .subscribeOn(Schedulers.io()) 93 | .observeOn(AndroidSchedulers.mainThread()) 94 | .subscribe(serviceAction, errorAction)); 95 | } 96 | mServices.put(createKey(service.getDomain(), service.getRegType(), service.getServiceName()), new BonjourDomain(service)); 97 | } 98 | } else { 99 | Log.e("TAG", "Unknown service protocol " + protocolSuffix); 100 | //Just ignore service with different protocol suffixes 101 | } 102 | } 103 | }; 104 | 105 | mDisposable = mRxDnssd.browse(Config.SERVICES_DOMAIN, Config.LOCAL_DOMAIN) 106 | .subscribeOn(Schedulers.io()) 107 | .subscribe(reqTypeAction, errorAction); 108 | } 109 | 110 | private static String createKey(String domain, String regType, String serviceName) { 111 | return domain + regType + serviceName; 112 | } 113 | 114 | public static class BonjourDomain extends BonjourService { 115 | public int serviceCount = 0; 116 | 117 | public BonjourDomain(BonjourService bonjourService){ 118 | super(new BonjourService.Builder(bonjourService)); 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/viewmodel/ServiceBrowserViewModel.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser.ui.viewmodel; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.lifecycle.AndroidViewModel; 7 | 8 | import com.druk.servicebrowser.BonjourApplication; 9 | import com.github.druk.rx2dnssd.BonjourService; 10 | import com.github.druk.rx2dnssd.Rx2Dnssd; 11 | 12 | import io.reactivex.android.schedulers.AndroidSchedulers; 13 | import io.reactivex.disposables.Disposable; 14 | import io.reactivex.functions.Consumer; 15 | import io.reactivex.schedulers.Schedulers; 16 | 17 | public class ServiceBrowserViewModel extends AndroidViewModel { 18 | 19 | protected Rx2Dnssd mRxDnssd; 20 | protected Disposable mDisposable; 21 | 22 | public ServiceBrowserViewModel(@NonNull Application application) { 23 | super(application); 24 | mRxDnssd = BonjourApplication.getRxDnssd(application); 25 | } 26 | 27 | @Override 28 | protected void onCleared() { 29 | super.onCleared(); 30 | if (mDisposable != null) { 31 | mDisposable.dispose(); 32 | } 33 | } 34 | 35 | public void startDiscovery(String reqType, 36 | String domain, 37 | Consumer servicesAction, 38 | Consumer errorAction) { 39 | mDisposable = mRxDnssd.browse(reqType, domain) 40 | .compose(mRxDnssd.resolve()) 41 | .subscribeOn(Schedulers.io()) 42 | .observeOn(AndroidSchedulers.mainThread()) 43 | .subscribe(servicesAction, errorAction); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/druk/servicebrowser/ui/viewmodel/ServiceDetailViewModel.java: -------------------------------------------------------------------------------- 1 | package com.druk.servicebrowser.ui.viewmodel; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.lifecycle.AndroidViewModel; 8 | 9 | import com.druk.servicebrowser.BonjourApplication; 10 | import com.github.druk.rx2dnssd.BonjourService; 11 | import com.github.druk.rx2dnssd.Rx2Dnssd; 12 | 13 | import java.io.IOException; 14 | import java.net.HttpURLConnection; 15 | import java.net.InetAddress; 16 | import java.net.MalformedURLException; 17 | import java.net.URL; 18 | import java.util.LinkedList; 19 | 20 | import io.reactivex.Observable; 21 | import io.reactivex.ObservableOnSubscribe; 22 | import io.reactivex.android.schedulers.AndroidSchedulers; 23 | import io.reactivex.disposables.Disposable; 24 | import io.reactivex.functions.Consumer; 25 | import io.reactivex.functions.Function; 26 | import io.reactivex.schedulers.Schedulers; 27 | 28 | public class ServiceDetailViewModel extends AndroidViewModel { 29 | 30 | private static final String HTTP_PROTOCOL = "http"; 31 | private static final String HTTPS_PROTOCOL = "https"; 32 | 33 | protected Rx2Dnssd mRxDnssd; 34 | 35 | private Disposable mResolveIPDisposable; 36 | private Disposable mResolveTXTDisposable; 37 | private Disposable mCheckHttpConnectionDisposable; 38 | 39 | public ServiceDetailViewModel(@NonNull Application application) { 40 | super(application); 41 | mRxDnssd = BonjourApplication.getRxDnssd(application); 42 | } 43 | 44 | @Override 45 | protected void onCleared() { 46 | super.onCleared(); 47 | if (mResolveIPDisposable != null) { 48 | mResolveIPDisposable.dispose(); 49 | } 50 | if (mResolveTXTDisposable != null) { 51 | mResolveTXTDisposable.dispose(); 52 | } 53 | if (mCheckHttpConnectionDisposable != null) { 54 | mCheckHttpConnectionDisposable.dispose(); 55 | } 56 | } 57 | 58 | public void resolveIPRecords(BonjourService service, Consumer consumer) { 59 | mResolveIPDisposable = mRxDnssd.queryIPRecords(service) 60 | .subscribeOn(Schedulers.io()) 61 | .observeOn(AndroidSchedulers.mainThread()) 62 | .subscribe(bonjourService -> { 63 | if (bonjourService.isLost()) { 64 | return; 65 | } 66 | consumer.accept(bonjourService); 67 | }, throwable -> Log.e("DNSSD", "Error: ", throwable)); 68 | } 69 | 70 | public void resolveTXTRecords(BonjourService service, Consumer consumer) { 71 | mResolveTXTDisposable = mRxDnssd.queryTXTRecords(service) 72 | .subscribeOn(Schedulers.io()) 73 | .observeOn(AndroidSchedulers.mainThread()) 74 | .subscribe(bonjourService -> { 75 | if (bonjourService.isLost()) { 76 | return; 77 | } 78 | consumer.accept(bonjourService); 79 | }, throwable -> Log.e("DNSSD", "Error: ", throwable)); 80 | } 81 | 82 | public void checkHttpConnection(BonjourService service, Consumer consumer) { 83 | if (mCheckHttpConnectionDisposable != null) { 84 | mCheckHttpConnectionDisposable.dispose(); 85 | } 86 | mCheckHttpConnectionDisposable = checkService(service) 87 | .observeOn(AndroidSchedulers.mainThread()) 88 | .subscribe(consumer); 89 | } 90 | 91 | private Observable checkService(@NonNull BonjourService service) { 92 | LinkedList urls = new LinkedList<>(); 93 | for (InetAddress inetAddress : service.getInetAddresses()) { 94 | try { 95 | urls.add(new URL(HTTP_PROTOCOL, inetAddress.getHostAddress(), service.getPort(), "")); 96 | } catch (MalformedURLException e) { 97 | e.printStackTrace(); 98 | } 99 | try { 100 | urls.add(new URL(HTTPS_PROTOCOL, inetAddress.getHostAddress(), service.getPort(), "")); 101 | } catch (MalformedURLException e) { 102 | e.printStackTrace(); 103 | } 104 | } 105 | 106 | Observable observable = Observable.fromIterable(urls); 107 | return observable.flatMap((Function>) url -> Observable.create((ObservableOnSubscribe) observableEmitter -> { 108 | boolean success = checkURL(url); 109 | if (success) { 110 | observableEmitter.onNext(url); 111 | observableEmitter.onComplete(); 112 | } 113 | })).take(1).subscribeOn(Schedulers.io()); 114 | } 115 | 116 | private boolean checkURL(@NonNull URL url) { 117 | try { 118 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 119 | connection.setRequestMethod("GET"); 120 | connection.connect(); 121 | int responseCode = connection.getResponseCode(); 122 | if (responseCode == HttpURLConnection.HTTP_OK) { 123 | return true; 124 | } 125 | } 126 | catch (IOException e) { 127 | e.printStackTrace(); 128 | } 129 | 130 | return false; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_autorenew.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_browser.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_report_problem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_input_antenna.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_accent.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_border.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout-sw600dp/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 28 | 29 | 33 | 34 | 35 | 36 | 41 | 42 | 49 | 50 | 57 | 58 | 63 | 64 | 78 | 79 | 88 | 89 | 90 | 91 | 101 | 102 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_html_viewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 27 | 28 | 32 | 33 | 34 | 35 | 39 | 40 | 44 | 45 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_license.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 29 | 30 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 30 | 31 | 35 | 36 | 37 | 38 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_reg_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 28 | 29 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 30 | 31 | 35 | 36 | 40 | 41 | 47 | 48 | 54 | 55 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 80 | 81 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/layout/blank_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | 36 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_add_txt_records.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_register_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 29 | 30 | 33 | 34 | 41 | 42 | 43 | 44 | 47 | 48 | 56 | 57 | 58 | 59 | 62 | 63 | 71 | 72 | 73 | 74 | 82 | 83 | 87 | 88 | 89 | 90 | 91 | 92 | 99 | 100 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_registrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 31 | 32 | 36 | 37 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_service_browser.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 32 | 33 | 36 | 37 | 38 | 39 | 46 | 47 | 52 | 53 | 60 | 61 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_service_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/one_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/two_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 27 | 28 | 34 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_bonjour_browser.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_bonjour_browser_for_developers.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_domain.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_registered_services.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andriydruk/BonjourBrowser/3d0822d92ba609932bf03755c3c3a27078395377/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-night-v23/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw720dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 320dp 19 | 650dp 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/values-v23/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 16dp 20 | 16dp 21 | 16dp 22 | 260dp 23 | 550dp 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | Service Browser 20 | Settings 21 | Service Browser 22 | 23 | No TXT records 24 | %1$s was copied to your clipboard 25 | Not resolved yet 26 | Service info was resolved 27 | "Domain: %1$s" 28 | "RegType: %1$s" 29 | "Last update: %1$s" 30 | Open-source licences 31 | Cannot open link 32 | 33 | local. 34 | No selected service 35 | Error occurred\nPlease send report to developer 36 | Send report 37 | 38 | Registrations 39 | No services registered 40 | Unregister all services 41 | Register new service 42 | Register service 43 | "Service name" 44 | "Reg type (e.g. _ftp._tcp)" 45 | "Port (0–63535)" 46 | TXT Records 47 | Key 48 | Value 49 | Add 50 | Register 51 | Mark as starred 52 | No services found 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 |