├── .circleci └── config.yml ├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── KeyEvent-Display---Android-AS.iml ├── KeyEvent-Display---Android.iml ├── LICENSE-2.0.html ├── README.md ├── apkdetails.sh ├── app ├── .gitignore ├── app.iml ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── aws │ │ └── apps │ │ └── keyeventdisplay │ │ ├── KeyEventApplication.java │ │ ├── deviceinfo │ │ ├── DeviceCapabilities.java │ │ └── DeviceInfoCollator.java │ │ └── ui │ │ ├── common │ │ ├── AlertDialogFragment.java │ │ ├── ColorProvider.java │ │ ├── DialogFactory.java │ │ └── NotifyUser.java │ │ ├── main │ │ ├── KeyEventConsumptionChecker.java │ │ ├── MainActivity.java │ │ ├── View.java │ │ ├── ViewState.java │ │ ├── export │ │ │ ├── DeviceInfo.java │ │ │ ├── Exporter.java │ │ │ └── WriteToDiskDelegate.java │ │ ├── logview │ │ │ ├── Line.java │ │ │ ├── LogViewWrapper.java │ │ │ └── SpannableTextFormatHelper.java │ │ └── markup │ │ │ ├── LineMarkup.java │ │ │ ├── LineMarkupFactory.java │ │ │ └── Tag.java │ │ └── tv │ │ └── TvActivity.java │ └── res │ ├── drawable-hdpi │ ├── ic_close.png │ ├── ic_info.png │ ├── ic_save.png │ └── ic_share.png │ ├── drawable-mdpi │ ├── ic_close.png │ ├── ic_info.png │ ├── ic_save.png │ └── ic_share.png │ ├── drawable-xhdpi │ ├── ic_close.png │ ├── ic_info.png │ ├── ic_save.png │ └── ic_share.png │ ├── drawable-xxhdpi │ ├── ic_close.png │ ├── ic_info.png │ ├── ic_save.png │ └── ic_share.png │ ├── drawable-xxxhdpi │ ├── ic_close.png │ ├── ic_info.png │ ├── ic_save.png │ └── ic_share.png │ ├── layout │ ├── activity_main.xml │ └── dialog_textview.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ ├── banner_android_tv.png │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ └── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── buildconstants ├── android-sdk-versions.gradle └── dependency-versions.gradle ├── buildsystem ├── android-defaults.gradle ├── apkdetails │ └── apkdetails-1.2.2.jar ├── codequality │ └── lint.xml ├── common-methods.gradle ├── generate_dependency_hashfile.sh ├── jacocoxml │ └── jacocoxmlparser-1.0.0.jar ├── multidex │ └── multidex.pro ├── proguard-rules │ ├── dagger2-rules.pro │ ├── gson-rules.pro │ └── kotlin-rules.pro └── signing_keys │ └── debug.keystore ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── KeyEventDisplay.svg └── KeyEventDisplay_512.png ├── import-summary.txt ├── keyevent-display.iml ├── monitors ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── uk │ └── co │ └── alt236 │ └── keyeventdisplay │ └── monitors │ ├── Filter.java │ ├── Monitor.java │ ├── MonitorCallback.java │ ├── kernel │ └── KernelLogMonitor.java │ ├── logcat │ └── LogCatMonitor.java │ └── util │ ├── ClosableHelper.java │ └── ProcessWrapper.java ├── print_codecov.sh └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | ## 4 | ## REFERENCES 5 | ################################################################ 6 | references: 7 | 8 | # 9 | # CONFIGURATION 10 | # 11 | workspace: &workspace 12 | ~/code 13 | 14 | environment_config: &environment_config 15 | working_directory: *workspace 16 | docker: 17 | - image: circleci/android:api-29 18 | environment: 19 | JVM_OPTS: -Xmx2048m 20 | GRADLE_OPTS: -Xmx1536m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false 21 | 22 | # 23 | # CACHE CONTROL 24 | # 25 | generate_dependency_hashfile: &generate_dependency_hashfile 26 | run: 27 | name: Generate Dependency Hashfile 28 | command: ./buildsystem/generate_dependency_hashfile.sh ./ ./dependency_hashfile.tmp 29 | 30 | print_dependency_hashfile: &print_dependency_hashfile 31 | run: 32 | name: Print Dependency Hashfile Contents 33 | command: cat ./dependency_hashfile.tmp 34 | 35 | cache_key: &cache_key 36 | key: cache-v1-{{ checksum "dependency_hashfile.tmp" }} 37 | 38 | cache_key_fallback: &cache_key_fallback 39 | key: cache-v1- 40 | 41 | restore_cache: &restore_cache 42 | restore_cache: 43 | <<: *cache_key 44 | <<: *cache_key_fallback 45 | 46 | save_cache: &save_cache 47 | save_cache: 48 | <<: *cache_key 49 | paths: 50 | - ~/.gradle 51 | - ~/.m2 52 | 53 | # 54 | # DOWNLOADING DEPENDENCIES 55 | # 56 | download_android_dependencies: &download_android_dependencies 57 | run: 58 | name: Download Dependencies 59 | command: ./gradlew dependencies androidDependencies 60 | 61 | # 62 | # RUNNING UNIT TESTS 63 | # 64 | run_android_lint: &run_android_lint 65 | run: 66 | name: Running lint 67 | command: ./gradlew lintDebug 68 | 69 | run_unit_tests: &run_unit_tests 70 | run: 71 | name: Running unit tests 72 | command: ./gradlew testDebug monitors:testDebug 73 | 74 | produce_code_coverage: &produce_code_coverage 75 | run: 76 | name: Producing Code Coverage 77 | command: ./gradlew jacocoTestReport 78 | 79 | print_code_coverage: &print_code_coverage 80 | run: 81 | name: Printing code coverage 82 | command: ./print_codecov.sh 83 | 84 | # 85 | # BUILDING DEBUG APKS (FOR FIREBASE) 86 | # 87 | build_debug_apks: &build_debug_apks 88 | run: 89 | name: Building Debug APKs 90 | command: | 91 | ./gradlew assembleDebug 92 | 93 | # 94 | # BUILDING RELEASE APKS 95 | # 96 | build_release_apks: &build_release_apks 97 | run: 98 | name: Building Release APKs 99 | command: | 100 | ./gradlew assembleRelease 101 | 102 | setup_environment: &setup_environment 103 | run: 104 | name: Setup environment 105 | command: | 106 | echo "$ENCODED_KEYSTORE" | base64 -di >> "${HOME}"/keystore.jks 107 | echo "$ENCODED_GPLAY_DEPLOY_KEY" | base64 -di --decode >> "${HOME}"/deployment_private_key.json 108 | echo "export ANDROID_KEYSTORE=${HOME}/keystore.jks" >> "$BASH_ENV" 109 | echo "export GPLAY_DEPLOY_KEY=${HOME}/deployment_private_key.json" >> "$BASH_ENV" 110 | 111 | ## 112 | ## COMMANDS 113 | ################################################################ 114 | commands: 115 | upload_test_results_and_artifacts: 116 | description: "Upload the test reports and artifacts for a given module" 117 | parameters: 118 | module_name: 119 | type: string 120 | default: "[MISSING]" 121 | steps: 122 | - store_artifacts: 123 | path: << parameters.module_name >>/build/reports 124 | destination: reports-<< parameters.module_name >> 125 | - store_test_results: 126 | path: << parameters.module_name >>/build/test-results 127 | 128 | upload_apks_to_ci: 129 | description: "Upload the APKs generated for a given module" 130 | parameters: 131 | module_name: 132 | type: string 133 | default: "[MISSING]" 134 | steps: 135 | - store_artifacts: 136 | path: << parameters.module_name >>/build/outputs/apk 137 | destination: apks-<< parameters.module_name >> 138 | 139 | print_apk_info: 140 | description: "Collect information of compiled APKs" 141 | parameters: 142 | module_name: 143 | type: string 144 | default: "[MISSING]" 145 | steps: 146 | - run: 147 | name: "Collect information of compiled APKs" 148 | when: on_success 149 | command: ./apkdetails.sh -i << parameters.module_name >>/build/outputs/apk/ -r -h -v 150 | 151 | setup_dependency_cache: 152 | description: "Restore (if present) populate and save (if needed) the dependency cache" 153 | steps: 154 | - *generate_dependency_hashfile 155 | - *print_dependency_hashfile 156 | - *restore_cache 157 | - *download_android_dependencies 158 | 159 | deploy_to_google_play: 160 | description: "Deploy APKs to Google Play" 161 | parameters: 162 | only_for_branch: 163 | type: string 164 | default: "[MISSING]" 165 | steps: 166 | - deploy: 167 | name: "Deploy to Google Play" 168 | when: on_success 169 | command: | 170 | if [ "${CIRCLE_BRANCH}" == "<< parameters.only_for_branch >>" ]; then 171 | ./gradlew app:publish 172 | else 173 | echo "Branch is not '<< parameters.only_for_branch >>'. Skipping deployment." 174 | fi 175 | 176 | ## 177 | ## JOBS 178 | ################################################################ 179 | jobs: 180 | local_test: 181 | <<: *environment_config 182 | steps: 183 | - checkout 184 | - *setup_environment 185 | - setup_dependency_cache 186 | - *run_unit_tests 187 | - *produce_code_coverage # Produce code coverage needs to be straight after running unit tests 188 | - *print_code_coverage 189 | - *run_android_lint 190 | - *build_debug_apks 191 | - *build_release_apks 192 | - *save_cache # Moved after test runs to capture dep downloads from the previous step 193 | - print_apk_info: 194 | module_name: "app" 195 | - persist_to_workspace: 196 | root: *workspace 197 | paths: 198 | - app/build/ 199 | - monitors/build/ 200 | - upload_test_results_and_artifacts: 201 | module_name: "app" 202 | - upload_test_results_and_artifacts: 203 | module_name: "monitors" 204 | - upload_apks_to_ci: 205 | module_name: "app" 206 | - deploy_to_google_play: 207 | only_for_branch: "master" 208 | 209 | workflows: 210 | version: 2.1 211 | build-and-deploy: 212 | jobs: 213 | - local_test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,java,android,androidstudio,intellij 4 | 5 | ### Android ### 6 | # Built application files 7 | *.apk 8 | *.aar 9 | *.ap_ 10 | *.aab 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | # Uncomment the following line in case you need and you don't have the release build type files in your app 23 | # release/ 24 | 25 | # Gradle files 26 | .gradle/ 27 | build/ 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | 38 | # Android Studio Navigation editor temp files 39 | .navigation/ 40 | 41 | # Android Studio captures folder 42 | captures/ 43 | 44 | # IntelliJ 45 | *.iml 46 | .idea/workspace.xml 47 | .idea/tasks.xml 48 | .idea/gradle.xml 49 | .idea/assetWizardSettings.xml 50 | .idea/dictionaries 51 | .idea/libraries 52 | # Android Studio 3 in .gitignore file. 53 | .idea/caches 54 | .idea/modules.xml 55 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 56 | .idea/navEditor.xml 57 | 58 | # Keystore files 59 | # Uncomment the following lines if you do not want to check your keystore files in. 60 | #*.jks 61 | #*.keystore 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | .cxx/ 66 | 67 | # Google Services (e.g. APIs or Firebase) 68 | # google-services.json 69 | 70 | # Freeline 71 | freeline.py 72 | freeline/ 73 | freeline_project_description.json 74 | 75 | # fastlane 76 | fastlane/report.xml 77 | fastlane/Preview.html 78 | fastlane/screenshots 79 | fastlane/test_output 80 | fastlane/readme.md 81 | 82 | # Version control 83 | vcs.xml 84 | 85 | # lint 86 | lint/intermediates/ 87 | lint/generated/ 88 | lint/outputs/ 89 | lint/tmp/ 90 | # lint/reports/ 91 | 92 | ### Android Patch ### 93 | gen-external-apklibs 94 | output.json 95 | 96 | # Replacement of .externalNativeBuild directories introduced 97 | # with Android Studio 3.5. 98 | 99 | ### Intellij ### 100 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 101 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 102 | 103 | # User-specific stuff 104 | .idea/**/workspace.xml 105 | .idea/**/tasks.xml 106 | .idea/**/usage.statistics.xml 107 | .idea/**/dictionaries 108 | .idea/**/shelf 109 | 110 | # Generated files 111 | .idea/**/contentModel.xml 112 | 113 | # Sensitive or high-churn files 114 | .idea/**/dataSources/ 115 | .idea/**/dataSources.ids 116 | .idea/**/dataSources.local.xml 117 | .idea/**/sqlDataSources.xml 118 | .idea/**/dynamic.xml 119 | .idea/**/uiDesigner.xml 120 | .idea/**/dbnavigator.xml 121 | 122 | # Gradle 123 | .idea/**/gradle.xml 124 | .idea/**/libraries 125 | 126 | # Gradle and Maven with auto-import 127 | # When using Gradle or Maven with auto-import, you should exclude module files, 128 | # since they will be recreated, and may cause churn. Uncomment if using 129 | # auto-import. 130 | .idea/artifacts 131 | .idea/compiler.xml 132 | .idea/jarRepositories.xml 133 | .idea/modules.xml 134 | .idea/*.iml 135 | .idea/modules 136 | *.iml 137 | *.ipr 138 | 139 | # CMake 140 | cmake-build-*/ 141 | 142 | # Mongo Explorer plugin 143 | .idea/**/mongoSettings.xml 144 | 145 | # File-based project format 146 | *.iws 147 | 148 | # IntelliJ 149 | 150 | # mpeltonen/sbt-idea plugin 151 | .idea_modules/ 152 | 153 | # JIRA plugin 154 | atlassian-ide-plugin.xml 155 | 156 | # Cursive Clojure plugin 157 | .idea/replstate.xml 158 | 159 | # Crashlytics plugin (for Android Studio and IntelliJ) 160 | com_crashlytics_export_strings.xml 161 | crashlytics.properties 162 | crashlytics-build.properties 163 | fabric.properties 164 | 165 | # Editor-based Rest Client 166 | .idea/httpRequests 167 | 168 | # Android studio 3.1+ serialized cache file 169 | .idea/caches/build_file_checksums.ser 170 | 171 | ### Intellij Patch ### 172 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 173 | 174 | # *.iml 175 | # modules.xml 176 | # .idea/misc.xml 177 | # *.ipr 178 | 179 | # Sonarlint plugin 180 | .idea/**/sonarlint/ 181 | 182 | # SonarQube Plugin 183 | .idea/**/sonarIssues.xml 184 | 185 | # Markdown Navigator plugin 186 | .idea/**/markdown-navigator.xml 187 | .idea/**/markdown-navigator-enh.xml 188 | .idea/**/markdown-navigator/ 189 | 190 | # Cache file creation bug 191 | # See https://youtrack.jetbrains.com/issue/JBR-2257 192 | .idea/$CACHE_FILE$ 193 | 194 | ### Java ### 195 | # Compiled class file 196 | 197 | # Log file 198 | 199 | # BlueJ files 200 | *.ctxt 201 | 202 | # Mobile Tools for Java (J2ME) 203 | .mtj.tmp/ 204 | 205 | # Package Files # 206 | *.jar 207 | *.war 208 | *.nar 209 | *.ear 210 | *.zip 211 | *.tar.gz 212 | *.rar 213 | 214 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 215 | hs_err_pid* 216 | 217 | ### Kotlin ### 218 | # Compiled class file 219 | 220 | # Log file 221 | 222 | # BlueJ files 223 | 224 | # Mobile Tools for Java (J2ME) 225 | 226 | # Package Files # 227 | 228 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 229 | 230 | ### AndroidStudio ### 231 | # Covers files to be ignored for android development using Android Studio. 232 | 233 | # Built application files 234 | 235 | # Files for the ART/Dalvik VM 236 | 237 | # Java class files 238 | 239 | # Generated files 240 | 241 | # Gradle files 242 | .gradle 243 | 244 | # Signing files 245 | .signing/ 246 | 247 | # Local configuration file (sdk path, etc) 248 | 249 | # Proguard folder generated by Eclipse 250 | 251 | # Log Files 252 | 253 | # Android Studio 254 | /*/build/ 255 | /*/local.properties 256 | /*/out 257 | /*/*/build 258 | /*/*/production 259 | *.ipr 260 | *~ 261 | *.swp 262 | 263 | # Android Patch 264 | 265 | # External native build folder generated in Android Studio 2.2 and later 266 | 267 | # NDK 268 | obj/ 269 | 270 | # IntelliJ IDEA 271 | /out/ 272 | 273 | # User-specific configurations 274 | .idea/caches/ 275 | .idea/libraries/ 276 | .idea/shelf/ 277 | .idea/.name 278 | .idea/compiler.xml 279 | .idea/copyright/profiles_settings.xml 280 | .idea/encodings.xml 281 | .idea/misc.xml 282 | .idea/scopes/scope_settings.xml 283 | .idea/vcs.xml 284 | .idea/jsLibraryMappings.xml 285 | .idea/datasources.xml 286 | .idea/dataSources.ids 287 | .idea/sqlDataSources.xml 288 | .idea/dynamic.xml 289 | .idea/uiDesigner.xml 290 | 291 | # OS-specific files 292 | .DS_Store 293 | .DS_Store? 294 | ._* 295 | .Spotlight-V100 296 | .Trashes 297 | ehthumbs.db 298 | Thumbs.db 299 | 300 | # Legacy Eclipse project files 301 | .classpath 302 | .project 303 | .cproject 304 | .settings/ 305 | 306 | # Mobile Tools for Java (J2ME) 307 | 308 | # Package Files # 309 | 310 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 311 | 312 | ## Plugin-specific files: 313 | 314 | # mpeltonen/sbt-idea plugin 315 | 316 | # JIRA plugin 317 | 318 | # Mongo Explorer plugin 319 | .idea/mongoSettings.xml 320 | 321 | # Crashlytics plugin (for Android Studio and IntelliJ) 322 | 323 | ### AndroidStudio Patch ### 324 | 325 | !/gradle/wrapper/gradle-wrapper.jar 326 | !/buildsystem/**/*.jar 327 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,intellij -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /KeyEvent-Display---Android-AS.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /KeyEvent-Display---Android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE-2.0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Apache License, Version 2.0 - The Apache Software Foundation 8 | 9 | 10 |

11 | Apache License
12 | Version 2.0, January 2004
13 | http://www.apache.org/licenses/ 14 |

15 |

16 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 17 |

18 |

1. Definitions.

19 |

20 | "License" shall mean the terms and conditions for use, reproduction, 21 | and distribution as defined by Sections 1 through 9 of this document. 22 |

23 |

24 | "Licensor" shall mean the copyright owner or entity authorized by 25 | the copyright owner that is granting the License. 26 |

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 |

37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 |

40 |

41 | "Source" form shall mean the preferred form for making modifications, 42 | including but not limited to software source code, documentation 43 | source, and configuration files. 44 |

45 |

46 | "Object" form shall mean any form resulting from mechanical 47 | transformation or translation of a Source form, including but 48 | not limited to compiled object code, generated documentation, 49 | and conversions to other media types. 50 |

51 |

52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 |

57 |

58 | "Derivative Works" shall mean any work, whether in Source or Object 59 | form, that is based on (or derived from) the Work and for which the 60 | editorial revisions, annotations, elaborations, or other modifications 61 | represent, as a whole, an original work of authorship. For the purposes 62 | of this License, Derivative Works shall not include works that remain 63 | separable from, or merely link (or bind by name) to the interfaces of, 64 | the Work and Derivative Works thereof. 65 |

66 |

67 | "Contribution" shall mean any work of authorship, including 68 | the original version of the Work and any modifications or additions 69 | to that Work or Derivative Works thereof, that is intentionally 70 | submitted to Licensor for inclusion in the Work by the copyright owner 71 | or by an individual or Legal Entity authorized to submit on behalf of 72 | the copyright owner. For the purposes of this definition, "submitted" 73 | means any form of electronic, verbal, or written communication sent 74 | to the Licensor or its representatives, including but not limited to 75 | communication on electronic mailing lists, source code control systems, 76 | and issue tracking systems that are managed by, or on behalf of, the 77 | Licensor for the purpose of discussing and improving the Work, but 78 | excluding communication that is conspicuously marked or otherwise 79 | designated in writing by the copyright owner as "Not a Contribution." 80 |

81 |

82 | "Contributor" shall mean Licensor and any individual or Legal Entity 83 | on behalf of whom a Contribution has been received by Licensor and 84 | subsequently incorporated within the Work. 85 |

86 |

2. Grant of Copyright License. 87 | Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | copyright license to reproduce, prepare Derivative Works of, 91 | publicly display, publicly perform, sublicense, and distribute the 92 | Work and such Derivative Works in Source or Object form. 93 |

94 |

3. Grant of Patent License. 95 | Subject to the terms and conditions of 96 | this License, each Contributor hereby grants to You a perpetual, 97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 98 | (except as stated in this section) patent license to make, have made, 99 | use, offer to sell, sell, import, and otherwise transfer the Work, 100 | where such license applies only to those patent claims licensable 101 | by such Contributor that are necessarily infringed by their 102 | Contribution(s) alone or by combination of their Contribution(s) 103 | with the Work to which such Contribution(s) was submitted. If You 104 | institute patent litigation against any entity (including a 105 | cross-claim or counterclaim in a lawsuit) alleging that the Work 106 | or a Contribution incorporated within the Work constitutes direct 107 | or contributory patent infringement, then any patent licenses 108 | granted to You under this License for that Work shall terminate 109 | as of the date such litigation is filed. 110 |

111 |

4. Redistribution. 112 | You may reproduce and distribute copies of the 113 | Work or Derivative Works thereof in any medium, with or without 114 | modifications, and in Source or Object form, provided that You 115 | meet the following conditions: 116 |

    117 |
  1. You must give any other recipients of the Work or 118 | Derivative Works a copy of this License; and 119 |

  2. 120 | 121 |
  3. You must cause any modified files to carry prominent notices 122 | stating that You changed the files; and 123 |

  4. 124 | 125 |
  5. You must retain, in the Source form of any Derivative Works 126 | that You distribute, all copyright, patent, trademark, and 127 | attribution notices from the Source form of the Work, 128 | excluding those notices that do not pertain to any part of 129 | the Derivative Works; and 130 |

  6. 131 | 132 |
  7. If the Work includes a "NOTICE" text file as part of its 133 | distribution, then any Derivative Works that You distribute must 134 | include a readable copy of the attribution notices contained 135 | within such NOTICE file, excluding those notices that do not 136 | pertain to any part of the Derivative Works, in at least one 137 | of the following places: within a NOTICE text file distributed 138 | as part of the Derivative Works; within the Source form or 139 | documentation, if provided along with the Derivative Works; or, 140 | within a display generated by the Derivative Works, if and 141 | wherever such third-party notices normally appear. The contents 142 | of the NOTICE file are for informational purposes only and 143 | do not modify the License. You may add Your own attribution 144 | notices within Derivative Works that You distribute, alongside 145 | or as an addendum to the NOTICE text from the Work, provided 146 | that such additional attribution notices cannot be construed 147 | as modifying the License.
  8. 148 |
149 | You may add Your own copyright statement to Your modifications and 150 | may provide additional or different license terms and conditions 151 | for use, reproduction, or distribution of Your modifications, or 152 | for any such Derivative Works as a whole, provided Your use, 153 | reproduction, and distribution of the Work otherwise complies with 154 | the conditions stated in this License. 155 | 156 |

5. Submission of Contributions. 157 | Unless You explicitly state otherwise, 158 | any Contribution intentionally submitted for inclusion in the Work 159 | by You to the Licensor shall be under the terms and conditions of 160 | this License, without any additional terms or conditions. 161 | Notwithstanding the above, nothing herein shall supersede or modify 162 | the terms of any separate license agreement you may have executed 163 | with Licensor regarding such Contributions. 164 |

165 |

6. Trademarks. 166 | This License does not grant permission to use the trade 167 | names, trademarks, service marks, or product names of the Licensor, 168 | except as required for reasonable and customary use in describing the 169 | origin of the Work and reproducing the content of the NOTICE file. 170 |

171 |

7. Disclaimer of Warranty. 172 | Unless required by applicable law or 173 | agreed to in writing, Licensor provides the Work (and each 174 | Contributor provides its Contributions) on an "AS IS" BASIS, 175 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 176 | implied, including, without limitation, any warranties or conditions 177 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 178 | PARTICULAR PURPOSE. You are solely responsible for determining the 179 | appropriateness of using or redistributing the Work and assume any 180 | risks associated with Your exercise of permissions under this License. 181 |

182 |

8. Limitation of Liability. 183 | In no event and under no legal theory, 184 | whether in tort (including negligence), contract, or otherwise, 185 | unless required by applicable law (such as deliberate and grossly 186 | negligent acts) or agreed to in writing, shall any Contributor be 187 | liable to You for damages, including any direct, indirect, special, 188 | incidental, or consequential damages of any character arising as a 189 | result of this License or out of the use or inability to use the 190 | Work (including but not limited to damages for loss of goodwill, 191 | work stoppage, computer failure or malfunction, or any and all 192 | other commercial damages or losses), even if such Contributor 193 | has been advised of the possibility of such damages. 194 |

195 |

9. Accepting Warranty or Additional Liability. 196 | While redistributing 197 | the Work or Derivative Works thereof, You may choose to offer, 198 | and charge a fee for, acceptance of support, warranty, indemnity, 199 | or other liability obligations and/or rights consistent with this 200 | License. However, in accepting such obligations, You may act only 201 | on Your own behalf and on Your sole responsibility, not on behalf 202 | of any other Contributor, and only if You agree to indemnify, 203 | defend, and hold each Contributor harmless for any liability 204 | incurred by, or claims asserted against, such Contributor by reason 205 | of your accepting any such warranty or additional liability. 206 |

207 |

208 | END OF TERMS AND CONDITIONS 209 |

210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KeyEvent Display - Android 2 | ======================== 3 | 4 | Once, I was playing with a number of Chinese tablets and trying different ROMs on them as they are technically the same hardware ([HSG X5A variants](http://www.huashiguang.com/MID.html)). 5 | 6 | I've had some problems getting the hard buttons to work though, so I wrote this application to detect key events and print them out. It will print out the following: 7 | 8 | * KeyEvents: The KeyEvents as Android understands them (KeyUp, KeyDown, KeyLongPress, KeyMultiple) 9 | 10 | * LogCat: Any relevant messages in logcat. Its filtered based on keywords declared in `arrays.xml` 11 | 12 | * Kernel: Any relevant messages in the kernel log. Its filtered based on keywords declared in `arrays.xml`. Kernel log parsing needs root. 13 | 14 | 15 | The three checkboxes at the top control what information will be displayed. 16 | 17 | 18 | * This is a personal debug tool, but I hope it will be of some use to someone else. 19 | 20 | * No Ads. 21 | 22 | Notes 23 | ----------- 24 | * While the application is running, the only "hard" keys which should work is "Home" and power. All others will produce their keycodes. 25 | 26 | * The SU request is to read the kernel log, so I can check if any keyevents are thrown by the kernel. 27 | 28 | * The location of keylayout files in Android is `/system/usr/keylayout`. 29 | 30 | * Kernel log parsing needs root 31 | 32 | * Both logcat and kernel log monitoring will only display lines containing words from two arrays in `arrays.xml` 33 | 34 | Currently the filters are: 35 | 36 | 37 | 1. Logcat: 38 | 1. HwGPIOE->GPDA 39 | 1. keycode 40 | 1. keycharacter 41 | 1. Kernel: 42 | 1. HwGPIOE->GPDA 43 | 1. keycode 44 | 1. keycharacter 45 | 46 | Permission Explanation 47 | -------------- 48 | * `READ_LOGS`: Used to access the Logcat log. 49 | * `WRITE_EXTERNAL_STORAGE`: Used to write the exported data to the SD card. 50 | 51 | Changelog 52 | -------------- 53 | * v0.0.1: First public release. 54 | * v0.0.2: Improved stability, added Exit button. 55 | * v0.0.3: Code updates. 56 | * v1.0.0: Redesign, added Android TV support. 57 | 58 | Links 59 | ------- 60 | * Market link: [https://market.android.com/details?id=aws.apps.keyeventdisplay](https://market.android.com/details?id=aws.apps.keyeventdisplay) 61 | * Webpage: [http://aschillings.co.uk/html/keyevent_display.html](http://aschillings.co.uk/html/keyevent_display.html) 62 | * Github: [https://github.com/alt236/KeyEvent-Display---Android](https://github.com/alt236/KeyEvent-Display---Android) 63 | 64 | Credits 65 | ------- 66 | 67 | Author: Alexandros Schillings. 68 | 69 | All logos are the property of their respective owners 70 | 71 | The code in this project is licensed under the [Apache Software License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 72 | 73 | Copyright (c) 2011 Alexandros Schillings. 74 | -------------------------------------------------------------------------------- /apkdetails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -o xtrace 5 | java -jar ./buildsystem/apkdetails/apkdetails-1.2.2.jar "$@" 6 | set +o xtrace -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'com.github.triplet.play' version '2.5.0' 4 | } 5 | 6 | apply from: "${project.rootDir}/buildsystem/android-defaults.gradle" 7 | 8 | android { 9 | 10 | final int buildNumber = getBuildNumber() 11 | final int versionMajor = 1 12 | final int versionMinor = 0 13 | final int versionPatch = buildNumber 14 | final int androidVersionCode = buildNumber 15 | 16 | final String semanticVersion = "${versionMajor}.${versionMinor}.${versionPatch}" 17 | 18 | signingConfigs { 19 | release { 20 | storeFile file(System.getenv("ANDROID_KEYSTORE") ?: "[KEY_NOT_DEFINED]") 21 | storePassword System.getenv("KEYSTORE_PASSWORD") 22 | keyAlias System.getenv("KEY_ALIAS") 23 | keyPassword System.getenv("KEY_PASSWORD") 24 | } 25 | 26 | debug { 27 | storeFile file("${project.rootDir}/buildsystem/signing_keys/debug.keystore") 28 | keyAlias 'androiddebugkey' 29 | keyPassword 'android' 30 | storePassword 'android' 31 | } 32 | } 33 | 34 | defaultConfig { 35 | versionCode androidVersionCode 36 | versionName semanticVersion 37 | 38 | buildConfigField "boolean", "STRICT_MODE", "false" 39 | } 40 | 41 | buildTypes { 42 | release { 43 | minifyEnabled false 44 | resValue "string", "app_name", "KeyEvent Display" 45 | if(isRunningOnCi()) { 46 | signingConfig signingConfigs.release 47 | } 48 | } 49 | 50 | debug { 51 | minifyEnabled false 52 | applicationIdSuffix ".debug" 53 | signingConfig signingConfigs.debug 54 | 55 | resValue "string", "app_name", "Debug KeyEvent Display" 56 | 57 | buildConfigField "boolean", "STRICT_MODE", "true" 58 | } 59 | } 60 | 61 | compileOptions { 62 | sourceCompatibility JavaVersion.VERSION_1_8 63 | targetCompatibility JavaVersion.VERSION_1_8 64 | } 65 | 66 | } 67 | 68 | dependencies { 69 | implementation project(':monitors') 70 | 71 | implementation "com.karumi:dexter:$dexter_version" 72 | implementation "androidx.appcompat:appcompat:$androidx_appcompat_version" 73 | implementation "androidx.annotation:annotation:$androidx_annotation_version" 74 | 75 | testImplementation "junit:junit:$junit_version" 76 | testImplementation "org.mockito:mockito-core:$mockito_version" 77 | } 78 | 79 | play { 80 | def credentialsPath = System.getenv("GPLAY_DEPLOY_KEY") ?: "[KEY_NOT_DEFINED]" 81 | def lastCommitMessage = getLastGitCommitMessage().take(50) 82 | 83 | logger.warn("GPP Config: $credentialsPath") 84 | logger.warn("Release Name: '$lastCommitMessage'") 85 | 86 | if(isRunningOnCi()) { 87 | enabled = true 88 | track = "internal" 89 | //userFraction = 1.0 90 | releaseStatus = "completed" 91 | serviceAccountCredentials = file(credentialsPath) 92 | releaseName = lastCommitMessage 93 | artifactDir = file("${project.rootDir}/app/build/outputs/apk/release/") 94 | } else { 95 | enabled = false 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/KeyEventApplication.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay; 2 | 3 | import android.app.Application; 4 | import android.os.StrictMode; 5 | import android.util.Log; 6 | 7 | public class KeyEventApplication extends Application { 8 | public void onCreate() { 9 | super.onCreate(); 10 | 11 | setUpStrictMode(); 12 | } 13 | 14 | private void setUpStrictMode() { 15 | Log.i("KeyEventDisplay", "Strict mode: " + BuildConfig.STRICT_MODE); 16 | 17 | if (BuildConfig.STRICT_MODE) { 18 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 19 | .detectAll() 20 | .penaltyLog() 21 | .build()); 22 | 23 | final StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder() 24 | .detectAll() 25 | .penaltyLog(); 26 | 27 | StrictMode.setVmPolicy(vmPolicyBuilder.build()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/deviceinfo/DeviceCapabilities.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.deviceinfo; 2 | 3 | import android.app.UiModeManager; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.os.Build; 7 | 8 | class DeviceCapabilities { 9 | private final Context context; 10 | 11 | public DeviceCapabilities(final Context context) { 12 | this.context = context; 13 | } 14 | 15 | public boolean isAndroidTv() { 16 | UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); 17 | return uiModeManager != null && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; 18 | } 19 | 20 | public boolean isAndroidWear() { 21 | Configuration config = context.getResources().getConfiguration(); 22 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) { 23 | return (config.uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_WATCH; 24 | } else { 25 | return false; 26 | } 27 | } 28 | 29 | public boolean isProbablyEmulator() { 30 | return Build.FINGERPRINT.startsWith("generic") 31 | || Build.FINGERPRINT.startsWith("unknown") 32 | || Build.MODEL.contains("google_sdk") 33 | || Build.MODEL.contains("Emulator") 34 | || Build.MODEL.contains("Android SDK built for x86") 35 | || Build.MANUFACTURER.contains("Genymotion") 36 | || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) 37 | || Build.PRODUCT.equals("google_sdk"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/deviceinfo/DeviceInfoCollator.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.deviceinfo; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | public class DeviceInfoCollator { 12 | private final DeviceCapabilities deviceCapabilities; 13 | 14 | public DeviceInfoCollator(Context context) { 15 | this.deviceCapabilities = new DeviceCapabilities(context); 16 | } 17 | 18 | @NonNull 19 | public Map collectDeviceInfo() { 20 | final Map retVal = new LinkedHashMap<>(); 21 | 22 | retVal.put("OS Release", pretty(Build.VERSION.RELEASE)); 23 | retVal.put("OS API Level", pretty(Build.VERSION.SDK_INT)); 24 | 25 | retVal.put("Manufacturer", pretty(Build.MANUFACTURER)); 26 | retVal.put("Brand", pretty(Build.BRAND)); 27 | retVal.put("Model", pretty(Build.MODEL)); 28 | 29 | retVal.put("Board", pretty(Build.BOARD)); 30 | retVal.put("CPU_ABI", pretty(Build.CPU_ABI)); 31 | retVal.put("CPU_ABI2", pretty(Build.CPU_ABI2)); 32 | retVal.put("Device", pretty(Build.DEVICE)); 33 | retVal.put("Display", pretty(Build.DISPLAY)); 34 | retVal.put("Host", pretty(Build.HOST)); 35 | retVal.put("ID", pretty(Build.ID)); 36 | 37 | retVal.put("Product", pretty(Build.PRODUCT)); 38 | retVal.put("Tags", pretty(Build.TAGS)); 39 | retVal.put("Type", pretty(Build.TYPE)); 40 | retVal.put("User", pretty(Build.USER)); 41 | retVal.put("Bootloader", pretty(Build.BOOTLOADER)); 42 | retVal.put("Hardware", pretty(Build.HARDWARE)); 43 | retVal.put("Radio", pretty(Build.RADIO)); 44 | 45 | retVal.put("Android TV", pretty(deviceCapabilities.isAndroidTv())); 46 | retVal.put("Android Wear", pretty(deviceCapabilities.isAndroidWear())); 47 | retVal.put("Probably Emulator", pretty(deviceCapabilities.isProbablyEmulator())); 48 | 49 | retVal.put("Fingerprint", pretty(Build.FINGERPRINT)); 50 | return retVal; 51 | } 52 | 53 | private String pretty(final String value) { 54 | if (value == null || "".equals(value)) { 55 | return "[value missing]"; 56 | } else { 57 | return value; 58 | } 59 | } 60 | 61 | private String pretty(final int value) { 62 | return String.valueOf(value); 63 | } 64 | 65 | private String pretty(final boolean value) { 66 | return String.valueOf(value); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/common/AlertDialogFragment.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.common; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.os.Bundle; 8 | import android.text.SpannableString; 9 | import android.text.method.LinkMovementMethod; 10 | import android.text.util.Linkify; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.widget.TextView; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.annotation.StringRes; 17 | import androidx.appcompat.app.AlertDialog; 18 | import androidx.fragment.app.DialogFragment; 19 | import aws.apps.keyeventdisplay.R; 20 | 21 | public class AlertDialogFragment extends DialogFragment { 22 | private static final String EXTRA_TITLE = AlertDialogFragment.class.getName() + ".EXTRA_TITLE"; 23 | private static final String EXTRA_MESSAGE = AlertDialogFragment.class.getName() + ".EXTRA_MESSAGE"; 24 | private static final String EXTRA_MODE = AlertDialogFragment.class.getName() + ".EXTRA_MODE"; 25 | private static final int MODE_RES = 0; 26 | private static final int MODE_STRING = 1; 27 | 28 | @Override 29 | public void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setRetainInstance(true); 32 | } 33 | 34 | @Override 35 | @NonNull 36 | public Dialog onCreateDialog(Bundle savedInstanceState) { 37 | final int mode = getArguments().getInt(EXTRA_MODE); 38 | 39 | final String title; 40 | final String message; 41 | 42 | if (mode == 0) { 43 | title = getString(getArguments().getInt(EXTRA_TITLE)); 44 | message = getString(getArguments().getInt(EXTRA_MESSAGE)); 45 | } else { 46 | title = getArguments().getString(EXTRA_TITLE); 47 | message = getArguments().getString(EXTRA_MESSAGE); 48 | } 49 | 50 | return createDialog(title, message); 51 | } 52 | 53 | 54 | private Dialog createDialog(final String title, final String message) { 55 | final Context context = getContext(); 56 | 57 | final View view = LayoutInflater.from(context).inflate(R.layout.dialog_textview, null); 58 | final TextView textView = view.findViewById(R.id.text); 59 | final SpannableString spannableMessage = new SpannableString(message); 60 | 61 | textView.setText(spannableMessage); 62 | textView.setAutoLinkMask(Activity.RESULT_OK); 63 | textView.setMovementMethod(LinkMovementMethod.getInstance()); 64 | 65 | Linkify.addLinks(spannableMessage, Linkify.ALL); 66 | 67 | final DialogInterface.OnClickListener listener = (dialog, id) -> { 68 | }; 69 | 70 | return new AlertDialog 71 | .Builder(context) 72 | .setTitle(title) 73 | .setPositiveButton(android.R.string.ok, listener) 74 | .setView(view) 75 | .create(); 76 | } 77 | 78 | @Override 79 | public void onDestroyView() { 80 | if (getDialog() != null && getRetainInstance()) { 81 | getDialog().setDismissMessage(null); 82 | } 83 | super.onDestroyView(); 84 | } 85 | 86 | @NonNull 87 | protected static DialogFragment newInstance(@StringRes final int title, 88 | @StringRes final int message) { 89 | final DialogFragment frag = new AlertDialogFragment(); 90 | final Bundle args = new Bundle(); 91 | 92 | args.putInt(EXTRA_TITLE, title); 93 | args.putInt(EXTRA_MESSAGE, message); 94 | args.putInt(EXTRA_MODE, MODE_RES); 95 | 96 | frag.setArguments(args); 97 | 98 | return frag; 99 | } 100 | 101 | @NonNull 102 | protected static DialogFragment newInstance(final String title, 103 | final String message) { 104 | final DialogFragment frag = new AlertDialogFragment(); 105 | final Bundle args = new Bundle(); 106 | 107 | args.putString(EXTRA_TITLE, title); 108 | args.putString(EXTRA_MESSAGE, message); 109 | args.putInt(EXTRA_MODE, MODE_STRING); 110 | 111 | frag.setArguments(args); 112 | 113 | return frag; 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/common/ColorProvider.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.common; 2 | 3 | import android.content.res.Resources; 4 | 5 | import androidx.annotation.ColorInt; 6 | import androidx.annotation.ColorRes; 7 | import androidx.core.content.res.ResourcesCompat; 8 | 9 | public class ColorProvider { 10 | private final Resources resources; 11 | private final Resources.Theme theme; 12 | 13 | public ColorProvider(final Resources resources, 14 | final Resources.Theme theme) { 15 | this.resources = resources; 16 | this.theme = theme; 17 | } 18 | 19 | @ColorInt 20 | public int getColor(@ColorRes final int resId) { 21 | return ResourcesCompat.getColor(resources, resId, theme); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/common/DialogFactory.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.common; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.fragment.app.DialogFragment; 6 | import aws.apps.keyeventdisplay.BuildConfig; 7 | import aws.apps.keyeventdisplay.R; 8 | 9 | public final class DialogFactory { 10 | 11 | public static DialogFragment createAboutDialog(final Context context) { 12 | final String title = context.getString(R.string.app_name) + " v" + BuildConfig.VERSION_NAME; 13 | final String message = getAboutText(context); 14 | return AlertDialogFragment.newInstance(title, message); 15 | } 16 | 17 | private static String getAboutText(final Context context) { 18 | String text = ""; 19 | 20 | text += context.getString(R.string.app_changelog); 21 | text += "\n\n"; 22 | text += context.getString(R.string.app_notes); 23 | text += "\n\n"; 24 | text += context.getString(R.string.app_acknowledgements); 25 | text += "\n\n"; 26 | text += context.getString(R.string.app_copyright); 27 | 28 | return text; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/common/NotifyUser.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.common; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import androidx.annotation.StringRes; 7 | 8 | public class NotifyUser { 9 | private final Context context; 10 | 11 | public NotifyUser(final Context context) { 12 | this.context = context.getApplicationContext(); 13 | } 14 | 15 | @Deprecated 16 | public void notifyLong(final CharSequence text) { 17 | Toast.makeText(context, text, Toast.LENGTH_LONG).show(); 18 | } 19 | 20 | public void notifyLong(@StringRes int stringResId) { 21 | Toast.makeText(context, stringResId, Toast.LENGTH_LONG).show(); 22 | } 23 | 24 | @Deprecated 25 | public void notifyShort(final CharSequence text) { 26 | Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); 27 | } 28 | 29 | public void notifyShort(@StringRes int stringResId) { 30 | Toast.makeText(context, stringResId, Toast.LENGTH_SHORT).show(); 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/KeyEventConsumptionChecker.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main; 2 | 3 | import android.view.KeyEvent; 4 | 5 | import java.util.Arrays; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | 9 | public class KeyEventConsumptionChecker { 10 | private static Integer[] ALLOWED_ITEMS = { 11 | KeyEvent.KEYCODE_DPAD_UP, 12 | KeyEvent.KEYCODE_DPAD_DOWN, 13 | KeyEvent.KEYCODE_DPAD_LEFT, 14 | KeyEvent.KEYCODE_DPAD_RIGHT}; 15 | private static final Set ALLOWED_ITEMS_SET = new HashSet<>(Arrays.asList(ALLOWED_ITEMS)); 16 | 17 | public boolean shouldConsumeEvent(final KeyEvent event) { 18 | return !ALLOWED_ITEMS_SET.contains(event.getKeyCode()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2012 Alexandros Schillings 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 aws.apps.keyeventdisplay.ui.main; 17 | 18 | import android.os.Bundle; 19 | import android.util.Log; 20 | import android.view.KeyEvent; 21 | 22 | import androidx.appcompat.app.AppCompatActivity; 23 | import aws.apps.keyeventdisplay.R; 24 | import aws.apps.keyeventdisplay.ui.common.ColorProvider; 25 | import aws.apps.keyeventdisplay.ui.common.DialogFactory; 26 | import aws.apps.keyeventdisplay.ui.common.NotifyUser; 27 | import aws.apps.keyeventdisplay.ui.main.export.Exporter; 28 | import aws.apps.keyeventdisplay.ui.main.export.WriteToDiskDelegate; 29 | import aws.apps.keyeventdisplay.ui.main.markup.LineMarkup; 30 | import aws.apps.keyeventdisplay.ui.main.markup.LineMarkupFactory; 31 | import uk.co.alt236.keyeventdisplay.monitors.Monitor; 32 | import uk.co.alt236.keyeventdisplay.monitors.MonitorCallback; 33 | import uk.co.alt236.keyeventdisplay.monitors.kernel.KernelLogMonitor; 34 | import uk.co.alt236.keyeventdisplay.monitors.logcat.LogCatMonitor; 35 | 36 | public class MainActivity extends AppCompatActivity { 37 | private final static String TAG = MainActivity.class.getSimpleName(); 38 | 39 | private static final int LAYOUT_ID = R.layout.activity_main; 40 | 41 | private WriteToDiskDelegate writeToDiskDelegate; 42 | private Monitor logCatMonitor; 43 | private Monitor kernelMonitor; 44 | private NotifyUser notifyUser; 45 | private Exporter exporter; 46 | private aws.apps.keyeventdisplay.ui.main.View view; 47 | private LineMarkupFactory markupFactory; 48 | private KeyEventConsumptionChecker keyEventConsumptionChecker; 49 | 50 | @Override 51 | public void onCreate(Bundle savedInstanceState) { 52 | super.onCreate(savedInstanceState); 53 | Log.d(TAG, "Starting: " + this.getClass().getName()); 54 | 55 | setContentView(LAYOUT_ID); 56 | 57 | final ColorProvider colorProvider = new ColorProvider(getResources(), getTheme()); 58 | 59 | markupFactory = new LineMarkupFactory(colorProvider); 60 | keyEventConsumptionChecker = new KeyEventConsumptionChecker(); 61 | 62 | view = new aws.apps.keyeventdisplay.ui.main.View(colorProvider); 63 | view.bind(this); 64 | 65 | view.setClearLogButtonListener(v -> view.clearLog()); 66 | view.setAppendBreakButtonListener(v -> view.appendBreakToLog()); 67 | view.setAboutButtonListener(v -> showAboutDialog()); 68 | view.setExitButtonListener(v -> quitApp()); 69 | view.setShareLogButtonListener(v -> shareLog()); 70 | view.setSaveLogButtonListener(v -> saveLogToDisk()); 71 | 72 | notifyUser = new NotifyUser(this); 73 | exporter = new Exporter(this, notifyUser); 74 | writeToDiskDelegate = new WriteToDiskDelegate(this, exporter, notifyUser); 75 | 76 | final String[] logCatFilter = getResources().getStringArray(R.array.logcat_filter); 77 | final String[] kernelFilter = getResources().getStringArray(R.array.kmsg_filter); 78 | logCatMonitor = new LogCatMonitor(logCatFilter); 79 | kernelMonitor = new KernelLogMonitor(kernelFilter); 80 | 81 | final Object lastState = getLastCustomNonConfigurationInstance(); 82 | if (lastState != null) { 83 | view.setState((ViewState) lastState); 84 | } 85 | } 86 | 87 | @Override 88 | public boolean onKeyDown(int keyCode, KeyEvent event) { 89 | final LineMarkup markup = markupFactory.getForKeyDown(); 90 | addKeyEventLine(markup, event); 91 | return keyEventConsumptionChecker.shouldConsumeEvent(event); 92 | } 93 | 94 | @Override 95 | public boolean onKeyLongPress(int keyCode, KeyEvent event) { 96 | final LineMarkup markup = markupFactory.getForKeyLongPress(); 97 | addKeyEventLine(markup, event); 98 | return keyEventConsumptionChecker.shouldConsumeEvent(event); 99 | } 100 | 101 | @Override 102 | public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { 103 | final LineMarkup markup = markupFactory.getForKeyMultiple(); 104 | addKeyEventLine(markup, event); 105 | return keyEventConsumptionChecker.shouldConsumeEvent(event); 106 | } 107 | 108 | @Override 109 | public boolean onKeyUp(int keyCode, KeyEvent event) { 110 | final LineMarkup markup = markupFactory.getForKeyUp(); 111 | addKeyEventLine(markup, event); 112 | return keyEventConsumptionChecker.shouldConsumeEvent(event); 113 | } 114 | 115 | @Override 116 | public Object onRetainCustomNonConfigurationInstance() { 117 | return view.getState(); 118 | } 119 | 120 | @Override 121 | public void onStart() { 122 | Log.d(TAG, "^ onStart called"); 123 | 124 | logCatMonitor.startMonitor(new MonitorCallback() { 125 | @Override 126 | public void onError(final String msg, final Throwable e) { 127 | runOnUiThread(() -> notifyUser.notifyLong("LogCatMonitor: " + msg + ": " + e)); 128 | } 129 | 130 | @Override 131 | public void onNewline(String line) { 132 | runOnUiThread(() -> addLogCatLine(line)); 133 | } 134 | }); 135 | 136 | kernelMonitor.startMonitor(new MonitorCallback() { 137 | @Override 138 | public void onError(final String msg, final Throwable e) { 139 | runOnUiThread(() -> notifyUser.notifyLong("KernelLogMonitor: " + msg + ": " + e)); 140 | } 141 | 142 | @Override 143 | public void onNewline(String line) { 144 | runOnUiThread(() -> addKernelLine(line)); 145 | } 146 | 147 | }); 148 | super.onStart(); 149 | } 150 | 151 | @Override 152 | public void onStop() { 153 | Log.d(TAG, "^ onStop called"); 154 | logCatMonitor.stopMonitor(); 155 | kernelMonitor.stopMonitor(); 156 | super.onStop(); 157 | } 158 | 159 | private void addKernelLine(String text) { 160 | if (view.isKernelChecked()) { 161 | final LineMarkup markup = markupFactory.getForKernel(); 162 | view.appendLogLine(markup.getTag(), text, markup.getColour()); 163 | } 164 | } 165 | 166 | private void addLogCatLine(String text) { 167 | if (view.isLogcatChecked()) { 168 | final LineMarkup markup = markupFactory.getForLogcat(); 169 | view.appendLogLine(markup.getTag(), text, markup.getColour()); 170 | } 171 | } 172 | 173 | private void addKeyEventLine(final LineMarkup markup, 174 | final KeyEvent event) { 175 | if (view.isKeyEventsChecked()) { 176 | view.appendKeyEvent(markup.getTag(), event, markup.getColour()); 177 | } 178 | } 179 | 180 | private void quitApp() { 181 | onStop(); 182 | System.exit(0); 183 | } 184 | 185 | private void showAboutDialog() { 186 | DialogFactory.createAboutDialog(this).show(getSupportFragmentManager(), "ALERT_DIALOG"); 187 | } 188 | 189 | private void saveLogToDisk() { 190 | final String text = view.getEventLogText().toString(); 191 | final boolean isSharable = isSharable(text); 192 | 193 | if (isSharable) { 194 | writeToDiskDelegate.saveLogToDisk(text); 195 | } else { 196 | notifyUser.notifyShort(R.string.nothing_to_save); 197 | } 198 | } 199 | 200 | private void shareLog() { 201 | final String text = view.getEventLogText().toString(); 202 | final boolean isSharable = isSharable(text); 203 | 204 | if (isSharable) { 205 | exporter.share(text); 206 | } else { 207 | notifyUser.notifyShort(R.string.nothing_to_share); 208 | } 209 | } 210 | 211 | private boolean isSharable(final String content) { 212 | final String startText = getString(R.string.greeting); 213 | 214 | return content != null 215 | && !content.isEmpty() 216 | && !startText.equals(content); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/View.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main; 2 | 3 | import android.app.Activity; 4 | import android.view.KeyEvent; 5 | import android.widget.Button; 6 | import android.widget.CheckBox; 7 | import android.widget.ImageButton; 8 | import android.widget.TextView; 9 | 10 | import aws.apps.keyeventdisplay.R; 11 | import aws.apps.keyeventdisplay.ui.common.ColorProvider; 12 | import aws.apps.keyeventdisplay.ui.main.logview.LogViewWrapper; 13 | 14 | class View { 15 | private final ColorProvider colorProvider; 16 | private LogViewWrapper eventLogWrapper; 17 | 18 | private CheckBox chkKernel; 19 | private CheckBox chkKeyEvents; 20 | private CheckBox chkLogcat; 21 | private Button btnClearLog; 22 | private Button btnAddBreakToLog; 23 | private ImageButton btnSaveLog; 24 | private ImageButton btnShareLog; 25 | private ImageButton btnAbout; 26 | private ImageButton btnExit; 27 | private TextView eventLog; 28 | 29 | View(final ColorProvider colorProvider) { 30 | this.colorProvider = colorProvider; 31 | } 32 | 33 | void bind(final Activity activity) { 34 | chkKernel = activity.findViewById(R.id.chkKernel); 35 | chkKeyEvents = activity.findViewById(R.id.chkKeyEvents); 36 | chkLogcat = activity.findViewById(R.id.chkLogCat); 37 | btnClearLog = activity.findViewById(R.id.btnClear); 38 | btnAddBreakToLog = activity.findViewById(R.id.btnBreak); 39 | btnSaveLog = activity.findViewById(R.id.btnSave); 40 | btnShareLog = activity.findViewById(R.id.btnShare); 41 | btnAbout = activity.findViewById(R.id.btnAbout); 42 | btnExit = activity.findViewById(R.id.btnExit); 43 | eventLog = activity.findViewById(R.id.fldEvent); 44 | 45 | eventLogWrapper = new LogViewWrapper(eventLog, colorProvider); 46 | } 47 | 48 | boolean isKernelChecked() { 49 | return chkKernel.isChecked(); 50 | } 51 | 52 | boolean isLogcatChecked() { 53 | return chkLogcat.isChecked(); 54 | } 55 | 56 | boolean isKeyEventsChecked() { 57 | return chkKeyEvents.isChecked(); 58 | } 59 | 60 | ViewState getState() { 61 | final ViewState state = new ViewState(); 62 | 63 | state.setChkKernel(isKernelChecked()); 64 | state.setChkLogcat(isLogcatChecked()); 65 | state.setChkKeyEvents(isKeyEventsChecked()); 66 | state.setLogText(eventLog.getText()); 67 | 68 | return state; 69 | } 70 | 71 | void setState(final ViewState viewState) { 72 | eventLog.setText(viewState.getLogText()); 73 | chkKernel.setChecked(viewState.isChkKernel()); 74 | chkKeyEvents.setChecked(viewState.isChkKeyEvents()); 75 | chkLogcat.setChecked(viewState.isChkLogcat()); 76 | eventLogWrapper.autoScroll(); 77 | } 78 | 79 | CharSequence getEventLogText() { 80 | return eventLog.getText(); 81 | } 82 | 83 | void appendLogLine(String title, String event, int color) { 84 | eventLogWrapper.appendLogLine(title, event, color); 85 | } 86 | 87 | void appendKeyEvent(String title, KeyEvent event, int color) { 88 | eventLogWrapper.appendKeyEvent(title, event, color); 89 | } 90 | 91 | void appendBreakToLog() { 92 | eventLogWrapper.appendBreak(); 93 | } 94 | 95 | void clearLog() { 96 | eventLogWrapper.clear(); 97 | } 98 | 99 | void setClearLogButtonListener(final android.view.View.OnClickListener listener) { 100 | btnClearLog.setOnClickListener(listener); 101 | } 102 | 103 | void setAppendBreakButtonListener(final android.view.View.OnClickListener listener) { 104 | btnAddBreakToLog.setOnClickListener(listener); 105 | } 106 | 107 | void setAboutButtonListener(final android.view.View.OnClickListener listener) { 108 | btnAbout.setOnClickListener(listener); 109 | } 110 | 111 | void setExitButtonListener(final android.view.View.OnClickListener listener) { 112 | btnExit.setOnClickListener(listener); 113 | } 114 | 115 | void setShareLogButtonListener(final android.view.View.OnClickListener listener) { 116 | btnShareLog.setOnClickListener(listener); 117 | } 118 | 119 | void setSaveLogButtonListener(final android.view.View.OnClickListener listener) { 120 | btnSaveLog.setOnClickListener(listener); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/ViewState.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2012 Alexandros Schillings 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 aws.apps.keyeventdisplay.ui.main; 17 | 18 | /*package*/class ViewState { 19 | private CharSequence logText; 20 | private boolean chkKeyEvents; 21 | private boolean chkLogcat; 22 | private boolean chkKernel; 23 | 24 | public CharSequence getLogText() { 25 | return logText; 26 | } 27 | 28 | public void setLogText(CharSequence logText) { 29 | this.logText = logText; 30 | } 31 | 32 | public boolean isChkKernel() { 33 | return chkKernel; 34 | } 35 | 36 | public void setChkKernel(boolean chkKernel) { 37 | this.chkKernel = chkKernel; 38 | } 39 | 40 | public boolean isChkKeyEvents() { 41 | return chkKeyEvents; 42 | } 43 | 44 | public void setChkKeyEvents(boolean chkKeyEvents) { 45 | this.chkKeyEvents = chkKeyEvents; 46 | } 47 | 48 | public boolean isChkLogcat() { 49 | return chkLogcat; 50 | } 51 | 52 | public void setChkLogcat(boolean chkLogcat) { 53 | this.chkLogcat = chkLogcat; 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/export/DeviceInfo.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.export; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.Map; 6 | 7 | import aws.apps.keyeventdisplay.deviceinfo.DeviceInfoCollator; 8 | 9 | /*package*/ final class DeviceInfo { 10 | 11 | private DeviceInfoCollator deviceInfoCollator; 12 | 13 | DeviceInfo(Context context) { 14 | this.deviceInfoCollator = new DeviceInfoCollator(context); 15 | } 16 | 17 | public void collectDeviceInfo(final StringBuilder sb) { 18 | final Map info = deviceInfoCollator.collectDeviceInfo(); 19 | 20 | for (final String key : info.keySet()) { 21 | append(sb, key + ": ", info.get(key), "\n"); 22 | } 23 | } 24 | 25 | private void append(final StringBuilder sb, 26 | final String val1, 27 | final String val2, 28 | final String val3) { 29 | sb.append(val1); 30 | sb.append(val2); 31 | sb.append(val3); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/export/Exporter.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.export; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Environment; 6 | import android.text.TextUtils; 7 | import android.util.Log; 8 | 9 | import java.io.BufferedWriter; 10 | import java.io.File; 11 | import java.io.FileWriter; 12 | import java.text.Format; 13 | import java.text.SimpleDateFormat; 14 | import java.util.Date; 15 | import java.util.Locale; 16 | 17 | import aws.apps.keyeventdisplay.R; 18 | import aws.apps.keyeventdisplay.ui.common.NotifyUser; 19 | 20 | public class Exporter { 21 | private static final String TAG = Exporter.class.getSimpleName(); 22 | private static final Format TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HHmmssZ", Locale.US); 23 | private static final String FILENAME_FORMATTER = "keyevent_%s.txt"; 24 | private static final String SUBJECT_FORMATTER = "keyevent_%s"; 25 | 26 | private final Activity activity; 27 | private final NotifyUser notifyUser; 28 | private final DeviceInfo deviceInfo; 29 | 30 | public Exporter(final Activity activity, NotifyUser notifyUser) { 31 | this.activity = activity; 32 | this.notifyUser = notifyUser; 33 | this.deviceInfo = new DeviceInfo(activity.getApplicationContext()); 34 | } 35 | 36 | public void save(CharSequence text) { 37 | final String exportableText = getExportText(text); 38 | if (isValidText(exportableText)) { 39 | final String time = getNow(); 40 | final String fileName = String.format(Locale.US, FILENAME_FORMATTER, time); 41 | final File directory = Environment.getExternalStorageDirectory(); 42 | 43 | saveToFile(fileName, directory, exportableText); 44 | } else { 45 | notifyUser.notifyShort(R.string.nothing_to_save); 46 | } 47 | } 48 | 49 | public void share(CharSequence text) { 50 | final String exportableText = getExportText(text); 51 | if (isValidText(exportableText)) { 52 | final String time = getNow(); 53 | final String subject = String.format(Locale.US, SUBJECT_FORMATTER, time); 54 | final Intent intent = createShareIntent(subject, exportableText); 55 | activity.startActivity(intent); 56 | } else { 57 | notifyUser.notifyShort(R.string.nothing_to_share); 58 | } 59 | } 60 | 61 | private String getExportText(final CharSequence text) { 62 | final StringBuilder sb = new StringBuilder(); 63 | deviceInfo.collectDeviceInfo(sb); 64 | sb.append("\n\n-----------------\n\n"); 65 | sb.append(text); 66 | return sb.toString(); 67 | } 68 | 69 | private void saveToFile(String fileName, File directory, String contents) { 70 | Log.d(TAG, "^ saveToFile: attempting to save at '" + directory.getAbsolutePath() + "/" + fileName + "'"); 71 | if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { 72 | 73 | try { 74 | if (directory.canWrite()) { 75 | final File gpxfile = new File(directory, fileName); 76 | final FileWriter gpxwriter = new FileWriter(gpxfile); 77 | final BufferedWriter out = new BufferedWriter(gpxwriter); 78 | out.write(contents); 79 | out.flush(); 80 | out.close(); 81 | 82 | final String pathAsString = gpxfile.getAbsolutePath(); 83 | Log.d(TAG, "^ saveToFile: saved as '" + pathAsString + "'"); 84 | notifyUser.notifyShort("Saved to SD as '" + pathAsString + "'"); 85 | } else { 86 | throw new IllegalStateException("Cannot write to directory: " + directory); 87 | } 88 | 89 | } catch (Exception e) { 90 | Log.e(TAG, "^ Could not write file. Error: " + e.getMessage(), e); 91 | notifyUser.notifyShort("Could not write file:\nError: " + e.getMessage()); 92 | } 93 | 94 | } else { 95 | notifyUser.notifyShort("No SD card is mounted..."); 96 | Log.e(TAG, "^ No SD card is mounted."); 97 | } 98 | } 99 | 100 | private Intent createShareIntent(String subject, String text) { 101 | final Intent intent = new Intent(Intent.ACTION_SEND); 102 | 103 | intent.setType("text/plain"); 104 | intent.putExtra(Intent.EXTRA_TEXT, text); 105 | intent.putExtra(Intent.EXTRA_SUBJECT, subject); 106 | intent.addCategory(Intent.CATEGORY_DEFAULT); 107 | 108 | return Intent.createChooser(intent, activity.getString(R.string.share_result_via)); 109 | } 110 | 111 | private static String getNow() { 112 | return TIME_FORMAT.format(new Date()); 113 | } 114 | 115 | private static boolean isValidText(final String text) { 116 | return !TextUtils.isEmpty(text); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/export/WriteToDiskDelegate.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.export; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.util.Log; 6 | 7 | import com.karumi.dexter.Dexter; 8 | import com.karumi.dexter.PermissionToken; 9 | import com.karumi.dexter.listener.PermissionDeniedResponse; 10 | import com.karumi.dexter.listener.PermissionGrantedResponse; 11 | import com.karumi.dexter.listener.PermissionRequest; 12 | import com.karumi.dexter.listener.single.PermissionListener; 13 | 14 | import aws.apps.keyeventdisplay.R; 15 | import aws.apps.keyeventdisplay.ui.common.NotifyUser; 16 | 17 | public class WriteToDiskDelegate { 18 | private static final String TAG = WriteToDiskDelegate.class.getSimpleName(); 19 | 20 | private final Activity activity; 21 | private final NotifyUser notifyUser; 22 | private final Exporter exporter; 23 | 24 | public WriteToDiskDelegate(Activity mainActivity, 25 | Exporter exporter, 26 | NotifyUser notifyUser) { 27 | this.activity = mainActivity; 28 | this.notifyUser = notifyUser; 29 | this.exporter = exporter; 30 | } 31 | 32 | public void saveLogToDisk(final String text) { 33 | Dexter.withContext(activity) 34 | .withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) 35 | .withListener(new PermissionListener() { 36 | @Override 37 | public void onPermissionGranted(final PermissionGrantedResponse response) { 38 | exporter.save(text); 39 | Log.i(TAG, "onPermissionGranted(" + response.getPermissionName() + ")"); 40 | } 41 | 42 | @Override 43 | public void onPermissionDenied(final PermissionDeniedResponse response) { 44 | notifyUser.notifyLong(R.string.toast_unable_to_save_log_to_disk); 45 | Log.e(TAG, "onPermissionDenied(" + response.getPermissionName() + ")"); 46 | } 47 | 48 | @Override 49 | public void onPermissionRationaleShouldBeShown(final PermissionRequest permission, final PermissionToken token) { 50 | notifyUser.notifyLong(R.string.toast_unable_to_save_log_to_disk); 51 | Log.w(TAG, "onPermissionRationaleShouldBeShown(" + permission.getName() + ")"); 52 | token.continuePermissionRequest(); 53 | } 54 | }) 55 | .withErrorListener(error -> Log.e(TAG, "Error when getting permission: " + error)) 56 | .check(); 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/logview/Line.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.logview; 2 | 3 | /*package*/ class Line { 4 | private final StringBuilder sb; 5 | private final int color; 6 | 7 | Line(final int color) { 8 | this.sb = new StringBuilder(); 9 | this.color = color; 10 | } 11 | 12 | public void append(final String string) { 13 | sb.append(string); 14 | } 15 | 16 | public void append(final String val1, 17 | final String val2) { 18 | append(val1); 19 | append(val2); 20 | } 21 | 22 | public void append(final String val1, 23 | final int val2) { 24 | append(val1); 25 | append(String.valueOf(val2)); 26 | } 27 | 28 | public void append(final String val1, 29 | final String val2, 30 | final String val3) { 31 | append(val1); 32 | append(val2); 33 | append(val3); 34 | } 35 | 36 | public void append(final String val1, 37 | final int val2, 38 | final String val3) { 39 | append(val1); 40 | append(String.valueOf(val2)); 41 | append(val3); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return sb.toString(); 47 | } 48 | 49 | public CharSequence getCharSequence() { 50 | return SpannableTextFormatHelper.color(color, sb); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/logview/LogViewWrapper.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.logview; 2 | 3 | import android.text.method.ScrollingMovementMethod; 4 | import android.view.KeyEvent; 5 | import android.widget.TextView; 6 | 7 | import aws.apps.keyeventdisplay.R; 8 | import aws.apps.keyeventdisplay.ui.common.ColorProvider; 9 | 10 | public class LogViewWrapper { 11 | private static final String NEW_LINE = "\n"; 12 | private static final String BREAK = "-------------------------" + NEW_LINE; 13 | private static final int MAX_TEXT_LINES = 1024; 14 | private final TextView textView; 15 | private final CharSequence breakLine; 16 | 17 | public LogViewWrapper(final TextView textView, 18 | final ColorProvider colorProvider) { 19 | this.textView = textView; 20 | this.textView.setMovementMethod(new ScrollingMovementMethod()); 21 | 22 | final int breakColor = colorProvider.getColor(R.color.lime); 23 | this.breakLine = SpannableTextFormatHelper.color(breakColor, BREAK); 24 | } 25 | 26 | public void appendKeyEvent(String title, KeyEvent event, int color) { 27 | final Line line = new Line(color); 28 | 29 | line.append(title); 30 | line.append("action=", event.getAction()); 31 | line.append(" code=", event.getKeyCode()); 32 | line.append(" repeat=", event.getRepeatCount()); 33 | line.append(" meta=", event.getMetaState()); 34 | line.append(" scanCode=", event.getScanCode()); 35 | line.append(" flags=", event.getFlags()); 36 | line.append(" label='", event.getDisplayLabel(), "'"); 37 | line.append(" chars='", event.getCharacters(), "'"); 38 | line.append(" number='", event.getNumber(), "'"); 39 | line.append(" deviceId='", event.getDeviceId(), "'"); 40 | line.append(" source='", event.getSource(), "'"); 41 | line.append("\n"); 42 | 43 | append(line.getCharSequence()); 44 | } 45 | 46 | public void appendLogLine(String title, String event, int color) { 47 | final Line line = new Line(color); 48 | 49 | line.append(title); 50 | line.append(event); 51 | line.append("\n"); 52 | 53 | append(line.getCharSequence()); 54 | } 55 | 56 | private void append(final CharSequence text) { 57 | sanityCheck(); 58 | textView.append(text); 59 | autoScroll(); 60 | } 61 | 62 | public void clear() { 63 | textView.setText(R.string.greeting); 64 | textView.scrollTo(0, 0); 65 | } 66 | 67 | public void appendBreak() { 68 | append(breakLine); 69 | } 70 | 71 | public CharSequence getText() { 72 | return textView.getText(); 73 | } 74 | 75 | public void autoScroll() { 76 | if (textView.getLineCount() == 0) { 77 | return; 78 | } 79 | 80 | final int maxLinesInView = textView.getHeight() / textView.getLineHeight(); 81 | 82 | if (maxLinesInView < textView.getLineCount()) { 83 | final int y = textView.getLineHeight() * textView.getLineCount() 84 | - (maxLinesInView * textView.getLineHeight()); 85 | textView.scrollTo(0, y); 86 | } 87 | } 88 | 89 | private void sanityCheck() { 90 | if (textView.getLineCount() > MAX_TEXT_LINES) { 91 | textView.setText(""); 92 | textView.scrollTo(0, 0); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/logview/SpannableTextFormatHelper.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.logview; 2 | 3 | import android.graphics.Typeface; 4 | import android.text.Spannable; 5 | import android.text.SpannableStringBuilder; 6 | import android.text.Spanned; 7 | import android.text.style.ForegroundColorSpan; 8 | import android.text.style.StyleSpan; 9 | 10 | /*package*/ final class SpannableTextFormatHelper { 11 | 12 | private SpannableTextFormatHelper() { 13 | // INSTANTIATION BAD 14 | } 15 | 16 | /** 17 | * Returns a CharSequence that concatenates the specified array of CharSequence 18 | * objects and then applies a list of zero or more tags to the entire range. 19 | * 20 | * @param content an array of character sequences to apply a style to 21 | * @param tags the styled span objects to apply to the content 22 | * such as android.text.style.StyleSpan 23 | */ 24 | private static CharSequence apply(final CharSequence[] content, final Object... tags) { 25 | final SpannableStringBuilder text = new SpannableStringBuilder(); 26 | openTags(text, tags); 27 | for (final CharSequence item : content) { 28 | text.append(item); 29 | } 30 | closeTags(text, tags); 31 | return text; 32 | } 33 | 34 | /** 35 | * Returns a CharSequence that applies boldface to the concatenation 36 | * of the specified CharSequence objects. 37 | */ 38 | @SuppressWarnings("unused") 39 | public static CharSequence bold(final CharSequence... content) { 40 | return apply(content, new StyleSpan(Typeface.BOLD)); 41 | } 42 | 43 | /** 44 | * Returns a CharSequence that applies bold and italics to the concatenation 45 | * of the specified CharSequence objects. 46 | */ 47 | @SuppressWarnings("unused") 48 | public static CharSequence boldItalic(final CharSequence... content) { 49 | return apply(content, new StyleSpan(Typeface.BOLD_ITALIC)); 50 | } 51 | 52 | /** 53 | * "Closes" the specified tags on a Spannable by updating the spans to be 54 | * endpoint-exclusive so that future text appended to the end will not take 55 | * on the same styling. Do not call this method directly. 56 | */ 57 | private static void closeTags(final Spannable text, final Object[] tags) { 58 | final int len = text.length(); 59 | for (final Object tag : tags) { 60 | if (len > 0) { 61 | text.setSpan(tag, 0, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 62 | } else { 63 | text.removeSpan(tag); 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Returns a CharSequence that applies a foreground color to the 70 | * concatenation of the specified CharSequence objects. 71 | */ 72 | @SuppressWarnings("unused") 73 | public static CharSequence color(final int color, final CharSequence... content) { 74 | return apply(content, new ForegroundColorSpan(color)); 75 | } 76 | 77 | /** 78 | * Returns a CharSequence that applies italics to the concatenation 79 | * of the specified CharSequence objects. 80 | */ 81 | @SuppressWarnings("unused") 82 | public static CharSequence italic(final CharSequence... content) { 83 | return apply(content, new StyleSpan(Typeface.ITALIC)); 84 | } 85 | 86 | /** 87 | * Returns a CharSequence that applies normal to the concatenation 88 | * of the specified CharSequence objects. 89 | */ 90 | @SuppressWarnings("unused") 91 | public static CharSequence normal(final CharSequence... content) { 92 | return apply(content, new StyleSpan(Typeface.NORMAL)); 93 | } 94 | 95 | /** 96 | * Iterates over an array of tags and applies them to the beginning of the specified 97 | * Spannable object so that future text appended to the text will have the styling 98 | * applied to it. Do not call this method directly. 99 | */ 100 | private static void openTags(final Spannable text, final Object[] tags) { 101 | for (final Object tag : tags) { 102 | text.setSpan(tag, 0, 0, Spannable.SPAN_MARK_MARK); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/markup/LineMarkup.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.markup; 2 | 3 | import androidx.annotation.ColorInt; 4 | 5 | public class LineMarkup { 6 | private final String tag; 7 | private final int colour; 8 | 9 | public LineMarkup(final String tag, 10 | @ColorInt final int colour) { 11 | this.tag = tag; 12 | this.colour = colour; 13 | } 14 | 15 | public String getTag() { 16 | return tag; 17 | } 18 | 19 | @ColorInt 20 | public int getColour() { 21 | return colour; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/markup/LineMarkupFactory.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.markup; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import aws.apps.keyeventdisplay.ui.common.ColorProvider; 7 | 8 | public class LineMarkupFactory { 9 | private static final String TAG_END = ": "; 10 | 11 | private final Map lineMarkupMap; 12 | 13 | public LineMarkupFactory(final ColorProvider colorProvider) { 14 | this.lineMarkupMap = new HashMap<>(); 15 | 16 | final int size = findLargestTagSize(); 17 | 18 | for (Tag tag : Tag.values()) { 19 | final String text = padRight(tag.getText(), size) + TAG_END; 20 | final int color = colorProvider.getColor(tag.getColorResId()); 21 | lineMarkupMap.put( 22 | tag, 23 | new LineMarkup(text, color)); 24 | } 25 | } 26 | 27 | public LineMarkup getForKeyDown() { 28 | return lineMarkupMap.get(Tag.KEY_DOWN); 29 | } 30 | 31 | public LineMarkup getForKeyUp() { 32 | return lineMarkupMap.get(Tag.KEY_UP); 33 | } 34 | 35 | public LineMarkup getForKeyLongPress() { 36 | return lineMarkupMap.get(Tag.KEY_LONGPRESS); 37 | } 38 | 39 | public LineMarkup getForKeyMultiple() { 40 | return lineMarkupMap.get(Tag.KEY_MULTIPLE); 41 | } 42 | 43 | public LineMarkup getForLogcat() { 44 | return lineMarkupMap.get(Tag.LOGCAT); 45 | } 46 | 47 | public LineMarkup getForKernel() { 48 | return lineMarkupMap.get(Tag.KERNEL); 49 | } 50 | 51 | private int findLargestTagSize() { 52 | int size = 0; 53 | for (Tag tag : Tag.values()) { 54 | if (tag.getText().length() > size) { 55 | size = tag.getText().length(); 56 | } 57 | } 58 | return size; 59 | } 60 | 61 | private String padRight(String str, int num) { 62 | return String.format("%1$-" + num + "s", str); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/main/markup/Tag.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.main.markup; 2 | 3 | import androidx.annotation.ColorRes; 4 | import aws.apps.keyeventdisplay.R; 5 | 6 | enum Tag { 7 | KERNEL("Kernel", R.color.color_kernel), 8 | LOGCAT("Logcat", R.color.color_logcat), 9 | KEY_DOWN("KeyDown", R.color.color_keydown), 10 | KEY_UP("KeyUp", R.color.color_keyup), 11 | KEY_LONGPRESS("KeyLongPress", R.color.color_keylongpress), 12 | KEY_MULTIPLE("KeyMultiple", R.color.color_keymultiple); 13 | 14 | private final String text; 15 | private final int colorResId; 16 | 17 | Tag(final String text, 18 | @ColorRes final int colorResId) { 19 | this.text = text; 20 | this.colorResId = colorResId; 21 | } 22 | 23 | public String getText() { 24 | return text; 25 | } 26 | 27 | @ColorRes 28 | public int getColorResId() { 29 | return colorResId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/aws/apps/keyeventdisplay/ui/tv/TvActivity.java: -------------------------------------------------------------------------------- 1 | package aws.apps.keyeventdisplay.ui.tv; 2 | 3 | import aws.apps.keyeventdisplay.ui.main.MainActivity; 4 | 5 | public class TvActivity extends MainActivity { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-hdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-hdpi/ic_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-hdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-hdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-mdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-mdpi/ic_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-mdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-mdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xhdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xhdpi/ic_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xhdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxhdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxhdpi/ic_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxhdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxxhdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxxhdpi/ic_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxxhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/KeyEvent-Display---Android/731d507258825790ce7a3bfd9031695cf03b27fa/app/src/main/res/drawable-xxxhdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 17 | 18 |