├── .github └── workflows │ └── blank.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE.md ├── a-log-repository-api ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── tech │ └── thdev │ └── android │ └── log │ └── repository │ └── api │ ├── ALogRepository.kt │ └── model │ └── ALogEntity.kt ├── a-log-repository-impl ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── tech │ │ └── thdev │ │ └── android │ │ └── log │ │ └── repository │ │ └── ALogRepositoryImpl.kt │ └── test │ └── java │ └── tech │ └── thdev │ └── android │ └── log │ └── repository │ └── ALogRepositoryImplTest.kt ├── a-log-timber ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── tech │ │ └── thdev │ │ └── android │ │ └── log │ │ └── repository │ │ └── timber │ │ └── ALogTimberDebug.kt │ └── test │ └── java │ └── tech │ └── thdev │ └── android │ └── log │ └── repository │ └── timber │ └── ALogTimberDebugTest.kt ├── a-log-view ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── tech │ │ │ └── thdev │ │ │ └── android │ │ │ └── log │ │ │ └── view │ │ │ ├── AComposeLifecycleServiceOwner.kt │ │ │ ├── ALogViewService.kt │ │ │ ├── ALogViewViewModel.kt │ │ │ ├── compose │ │ │ ├── ALogMessageScreen.kt │ │ │ ├── ALogScreen.kt │ │ │ └── ui │ │ │ │ └── theme │ │ │ │ └── ALogColors.kt │ │ │ ├── coroutines │ │ │ └── ADispatcherProvider.kt │ │ │ └── model │ │ │ ├── ADragItem.kt │ │ │ └── ALogItem.kt │ └── res │ │ └── drawable │ │ ├── baseline_adb_24.xml │ │ ├── baseline_cancel_24.xml │ │ └── baseline_settings_24.xml │ └── test │ └── java │ └── tech │ └── thdev │ └── android │ └── log │ └── view │ ├── ALogViewViewModelTest.kt │ └── coroutines │ └── MockADispatcherProvider.kt ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── tech │ │ └── thdev │ │ └── android │ │ └── logview │ │ └── example │ │ ├── MainActivity.kt │ │ ├── compose │ │ └── MainScreen.kt │ │ └── ui │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── tech │ └── thdev │ └── gradle │ ├── dependencies │ └── Publish.kt │ └── locals │ └── base │ ├── lib-publish-android.gradle.kts │ └── lib-publish.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── sample.gif └── settings.gradle.kts /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: ALogViewApp 2 | on: 3 | push: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: set up JDK 17 12 | uses: actions/setup-java@v3 13 | with: 14 | distribution: adopt 15 | java-version: 17 16 | 17 | - name: Build 18 | run: ./gradlew assembleDebug -Papp.enableComposeCompilerReports=true 19 | 20 | - name: Compose Metrics 21 | uses: lhoyong/android-compose-metrics-action@v1 22 | with: 23 | directory: 'compose_metrics' # see sample project app/build.gradle.kts 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | build.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 thdev.tech 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | Floating Android Log view 4 | 5 | Use gradle - compose 1.4.0, compose compiler 1.4.4, kotlin 1.8.10 6 | 7 | ## Use Code 8 | 9 | ```kotlin 10 | if (Settings.canDrawOverlays(this@MainActivity).not()) { 11 | // Add executable code after checking permissions 12 | startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))) 13 | } else { 14 | startService(ALogViewService.newInstance(this@MainActivity)) 15 | } 16 | ``` 17 | 18 | ## Sample 19 | 20 | ![image](images/sample.gif) -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0-alpha01 2 | 3 | Log view using Timber log 4 | Using floating UI and overlay UI -------------------------------------------------------------------------------- /a-log-repository-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /a-log-repository-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import tech.thdev.gradle.dependencies.Publish 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("lib-publish-android") 9 | } 10 | 11 | ext["libraryName"] = "a-log-repository-api" 12 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 13 | ext["description"] = Publish.description 14 | ext["url"] = Publish.publishUrl 15 | 16 | android { 17 | namespace = "tech.thdev.android.log.repository.api" 18 | buildToolsVersion = libs.versions.buildToolsVersion.get() 19 | compileSdk = libs.versions.compileSdk.get().toInt() 20 | 21 | defaultConfig { 22 | minSdk = libs.versions.minSdk.get().toInt() 23 | targetSdk = libs.versions.targetSdk.get().toInt() 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 32 | } 33 | 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation(libs.kotlin.stdlib) 52 | implementation(libs.coroutines.core) 53 | } -------------------------------------------------------------------------------- /a-log-repository-api/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/a-log-repository-api/consumer-rules.pro -------------------------------------------------------------------------------- /a-log-repository-api/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /a-log-repository-api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /a-log-repository-api/src/main/java/tech/thdev/android/log/repository/api/ALogRepository.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository.api 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import tech.thdev.android.log.repository.api.model.ALogEntity 5 | 6 | interface ALogRepository { 7 | 8 | fun runLog() 9 | 10 | fun stopLog() 11 | 12 | fun flowLog(): Flow> 13 | 14 | suspend fun getLastLog(takeLast: Int): List 15 | 16 | fun updateLog(priority: Int, tag: String?, message: String, t: Throwable?) 17 | } -------------------------------------------------------------------------------- /a-log-repository-api/src/main/java/tech/thdev/android/log/repository/api/model/ALogEntity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository.api.model 2 | 3 | data class ALogEntity( 4 | val priority: Int, 5 | val tag: String?, 6 | val message: String, 7 | val t: Throwable?, 8 | ) 9 | -------------------------------------------------------------------------------- /a-log-repository-impl/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /a-log-repository-impl/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import tech.thdev.gradle.dependencies.Publish 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("lib-publish-android") 9 | } 10 | 11 | ext["libraryName"] = "a-log-repository-impl" 12 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 13 | ext["description"] = Publish.description 14 | ext["url"] = Publish.publishUrl 15 | 16 | android { 17 | namespace = "tech.thdev.android.log.repository" 18 | buildToolsVersion = libs.versions.buildToolsVersion.get() 19 | compileSdk = libs.versions.compileSdk.get().toInt() 20 | 21 | defaultConfig { 22 | minSdk = libs.versions.minSdk.get().toInt() 23 | targetSdk = libs.versions.targetSdk.get().toInt() 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 32 | } 33 | 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | 49 | testOptions { 50 | unitTests.all { 51 | it.useJUnitPlatform() 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation(libs.kotlin.stdlib) 58 | implementation(libs.coroutines.core) 59 | implementation(projects.aLogRepositoryApi) 60 | 61 | testImplementation(libs.test.junit5) 62 | testRuntimeOnly(libs.test.junit5.engine) 63 | testImplementation(libs.test.mockito.kotlin) 64 | testImplementation(libs.test.coroutines) 65 | testImplementation(libs.test.coroutines.turbine) 66 | } -------------------------------------------------------------------------------- /a-log-repository-impl/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/a-log-repository-impl/consumer-rules.pro -------------------------------------------------------------------------------- /a-log-repository-impl/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /a-log-repository-impl/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /a-log-repository-impl/src/main/java/tech/thdev/android/log/repository/ALogRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.filter 6 | import kotlinx.coroutines.flow.onEach 7 | import tech.thdev.android.log.repository.api.ALogRepository 8 | import tech.thdev.android.log.repository.api.model.ALogEntity 9 | 10 | class ALogRepositoryImpl : ALogRepository { 11 | 12 | internal var sendLog = false 13 | 14 | private var cacheList = mutableListOf() 15 | 16 | override fun runLog() { 17 | sendLog = true 18 | } 19 | 20 | override fun stopLog() { 21 | sendLog = false 22 | } 23 | 24 | private val logSharedFlow = MutableStateFlow>(emptyList()) 25 | 26 | override suspend fun getLastLog(takeLast: Int): List = 27 | synchronized(cacheList) { 28 | cacheList 29 | .reversed() 30 | .filter { 31 | it.message.isNotEmpty() 32 | } 33 | .take(takeLast) 34 | .reversed() 35 | } 36 | 37 | override fun flowLog(): Flow> = 38 | logSharedFlow 39 | .filter { sendLog } 40 | .onEach { 41 | if (it.size == MAX_LOG_COUNT) { 42 | cacheList.clear() 43 | } 44 | } 45 | 46 | override fun updateLog(priority: Int, tag: String?, message: String, t: Throwable?) { 47 | synchronized(cacheList) { 48 | cacheList.add( 49 | ALogEntity( 50 | priority = priority, 51 | tag = tag, 52 | message = message, 53 | t = t, 54 | ) 55 | ) 56 | 57 | logSharedFlow.value = cacheList.toList() 58 | } 59 | } 60 | 61 | companion object { 62 | 63 | const val MAX_LOG_COUNT = 500 64 | } 65 | } -------------------------------------------------------------------------------- /a-log-repository-impl/src/test/java/tech/thdev/android/log/repository/ALogRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository 2 | 3 | import app.cash.turbine.test 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.runTest 6 | import org.junit.jupiter.api.Assertions 7 | import org.junit.jupiter.api.Test 8 | import tech.thdev.android.log.repository.api.model.ALogEntity 9 | 10 | internal class ALogRepositoryImplTest { 11 | 12 | private val repository = ALogRepositoryImpl() 13 | 14 | @Test 15 | fun `test changeUseLog`() { 16 | Assertions.assertFalse(repository.sendLog) 17 | repository.runLog() 18 | Assertions.assertTrue(repository.sendLog) 19 | repository.stopLog() 20 | Assertions.assertFalse(repository.sendLog) 21 | } 22 | 23 | @OptIn(ExperimentalCoroutinesApi::class) 24 | @Test 25 | fun `test flowLog`() = runTest { 26 | repository.flowLog() 27 | .test { 28 | expectNoEvents() 29 | 30 | // Not use log 31 | repository.updateLog(0, "", "New message", null) 32 | expectNoEvents() 33 | 34 | repository.runLog() 35 | repository.updateLog(0, "", "message 2", null) 36 | val newList = listOf( 37 | ALogEntity(0, "", "New message", null), 38 | ALogEntity(0, "", "message 2", null), 39 | ) 40 | Assertions.assertEquals(newList, awaitItem()) 41 | 42 | // Last copy test 43 | Assertions.assertEquals(newList, repository.getLastLog(30)) 44 | 45 | cancelAndConsumeRemainingEvents() 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /a-log-timber/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /a-log-timber/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import tech.thdev.gradle.dependencies.Publish 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("lib-publish-android") 9 | } 10 | 11 | ext["libraryName"] = "a-log-repository-timber" 12 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 13 | ext["description"] = Publish.description 14 | ext["url"] = Publish.publishUrl 15 | 16 | android { 17 | namespace = "tech.thdev.android.log.repository.timber" 18 | buildToolsVersion = libs.versions.buildToolsVersion.get() 19 | compileSdk = libs.versions.compileSdk.get().toInt() 20 | 21 | defaultConfig { 22 | minSdk = libs.versions.minSdk.get().toInt() 23 | targetSdk = libs.versions.targetSdk.get().toInt() 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 32 | } 33 | 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | 49 | testOptions { 50 | unitTests.all { 51 | it.useJUnitPlatform() 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation(libs.kotlin.stdlib) 58 | implementation(libs.coroutines.core) 59 | implementation(libs.timber) 60 | implementation(projects.aLogRepositoryApi) 61 | 62 | testImplementation(libs.test.junit5) 63 | testRuntimeOnly(libs.test.junit5.engine) 64 | testImplementation(libs.test.mockito.kotlin) 65 | } -------------------------------------------------------------------------------- /a-log-timber/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/a-log-timber/consumer-rules.pro -------------------------------------------------------------------------------- /a-log-timber/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /a-log-timber/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /a-log-timber/src/main/java/tech/thdev/android/log/repository/timber/ALogTimberDebug.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository.timber 2 | 3 | import tech.thdev.android.log.repository.api.ALogRepository 4 | import timber.log.Timber 5 | 6 | class ALogTimberDebug( 7 | private val aLogRepository: ALogRepository, 8 | ) : Timber.Tree() { 9 | 10 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 11 | aLogRepository.updateLog(priority, tag, message, t) 12 | } 13 | } -------------------------------------------------------------------------------- /a-log-timber/src/test/java/tech/thdev/android/log/repository/timber/ALogTimberDebugTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.repository.timber 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.mockito.kotlin.mock 5 | import tech.thdev.android.log.repository.api.ALogRepository 6 | 7 | internal class ALogTimberDebugTest { 8 | 9 | private val aLogRepository = mock() 10 | 11 | private val timber = ALogTimberDebug(aLogRepository) 12 | 13 | @Test 14 | fun `test updateLog`() { 15 | timber.log(0, "message") 16 | aLogRepository.updateLog(0, tag = null, message = "message", t = null) 17 | } 18 | } -------------------------------------------------------------------------------- /a-log-view/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /a-log-view/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import tech.thdev.gradle.dependencies.Publish 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("lib-publish-android") 9 | } 10 | 11 | ext["libraryName"] = "a-log-view" 12 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 13 | ext["description"] = Publish.description 14 | ext["url"] = Publish.publishUrl 15 | 16 | android { 17 | namespace = "tech.thdev.android.log.view" 18 | buildToolsVersion = libs.versions.buildToolsVersion.get() 19 | compileSdk = libs.versions.compileSdk.get().toInt() 20 | 21 | defaultConfig { 22 | minSdk = libs.versions.minSdk.get().toInt() 23 | targetSdk = libs.versions.targetSdk.get().toInt() 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 32 | } 33 | 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | 49 | buildFeatures { 50 | compose = true 51 | } 52 | 53 | composeOptions { 54 | kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get() 55 | } 56 | 57 | testOptions { 58 | unitTests.all { 59 | it.useJUnitPlatform() 60 | } 61 | } 62 | } 63 | 64 | dependencies { 65 | implementation(libs.kotlin.stdlib) 66 | implementation(libs.kotlin.immutable) 67 | implementation(libs.androidx.core) 68 | implementation(libs.androidx.appCompat) 69 | implementation(libs.compose.ui) 70 | implementation(libs.compose.material) 71 | implementation(libs.compose.runtime) 72 | implementation(libs.compose.foundation) 73 | implementation(libs.compose.uiToolingPreview) 74 | implementation(libs.compose.constraintLayout) 75 | debugRuntimeOnly(libs.compose.uiTooling) 76 | 77 | implementation(libs.timber) 78 | 79 | implementation(projects.aLogRepositoryApi) 80 | implementation(projects.aLogRepositoryImpl) 81 | implementation(projects.aLogTimber) 82 | 83 | testImplementation(libs.test.junit5) 84 | testRuntimeOnly(libs.test.junit5.engine) 85 | testImplementation(libs.test.mockito.kotlin) 86 | testImplementation(libs.test.coroutines) 87 | testImplementation(libs.test.coroutines.turbine) 88 | } -------------------------------------------------------------------------------- /a-log-view/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/a-log-view/consumer-rules.pro -------------------------------------------------------------------------------- /a-log-view/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /a-log-view/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/AComposeLifecycleServiceOwner.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleRegistry 6 | import androidx.savedstate.SavedStateRegistry 7 | import androidx.savedstate.SavedStateRegistryController 8 | import androidx.savedstate.SavedStateRegistryOwner 9 | 10 | class AComposeLifecycleServiceOwner : SavedStateRegistryOwner { 11 | 12 | private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) 13 | private var savedStateRegistryController: SavedStateRegistryController = 14 | SavedStateRegistryController.create(this) 15 | 16 | override val lifecycle: Lifecycle 17 | get() = lifecycleRegistry 18 | 19 | fun handleLifecycleEvent(event: Lifecycle.Event) { 20 | lifecycleRegistry.handleLifecycleEvent(event) 21 | } 22 | 23 | override val savedStateRegistry: SavedStateRegistry 24 | get() = savedStateRegistryController.savedStateRegistry 25 | 26 | fun performRestore(savedState: Bundle?) { 27 | savedStateRegistryController.performRestore(savedState) 28 | } 29 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/ALogViewService.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.PixelFormat 7 | import android.os.Build 8 | import android.os.IBinder 9 | import android.provider.Settings 10 | import android.view.Gravity 11 | import android.view.WindowManager 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.runtime.State 16 | import androidx.compose.runtime.collectAsState 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.platform.ComposeView 20 | import androidx.core.content.ContextCompat 21 | import androidx.lifecycle.Lifecycle 22 | import androidx.lifecycle.setViewTreeLifecycleOwner 23 | import androidx.savedstate.setViewTreeSavedStateRegistryOwner 24 | import tech.thdev.android.log.repository.ALogRepositoryImpl 25 | import tech.thdev.android.log.repository.timber.ALogTimberDebug 26 | import tech.thdev.android.log.view.compose.ALogMessageScreen 27 | import tech.thdev.android.log.view.compose.ALogScreen 28 | import tech.thdev.android.log.view.coroutines.ADispatcherProvider 29 | import tech.thdev.android.log.view.model.ADragItem 30 | import timber.log.Timber 31 | 32 | class ALogViewService : Service() { 33 | 34 | private val aLogRepository by lazy { 35 | ALogRepositoryImpl() 36 | } 37 | 38 | private val aLogViewViewModel by lazy { 39 | ALogViewViewModel( 40 | aLogRepository = aLogRepository, 41 | dispatcherProvider = ADispatcherProvider.invoke(), 42 | ) 43 | } 44 | 45 | private val windowManager by lazy { 46 | ContextCompat.getSystemService(this, WindowManager::class.java) 47 | } 48 | 49 | private val params by lazy { 50 | WindowManager.LayoutParams().apply { 51 | flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 52 | type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 53 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 54 | } else { 55 | WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 56 | } 57 | width = WindowManager.LayoutParams.WRAP_CONTENT 58 | height = WindowManager.LayoutParams.WRAP_CONTENT 59 | gravity = Gravity.TOP or Gravity.START 60 | format = PixelFormat.TRANSLUCENT 61 | } 62 | } 63 | 64 | private var composeView: ComposeView? = null 65 | 66 | override fun onBind(intent: Intent?): IBinder? { 67 | TODO("Not yet implemented") 68 | } 69 | 70 | override fun onCreate() { 71 | super.onCreate() 72 | Timber.plant(ALogTimberDebug(aLogRepository)) // Timber append. 73 | if (Settings.canDrawOverlays(this)) { 74 | createComposeView() 75 | aLogViewViewModel.run() 76 | } else { 77 | stopSelf() 78 | } 79 | } 80 | 81 | override fun onDestroy() { 82 | super.onDestroy() 83 | 84 | composeView?.let { // Remove view 85 | windowManager?.removeViewImmediate(it) 86 | } 87 | } 88 | 89 | private fun createComposeView() { 90 | composeView = ComposeView(this).apply { 91 | setContent { 92 | CompositionLocalProvider { 93 | ContentView(params, windowManager) 94 | } 95 | } 96 | } 97 | 98 | val composeLifecycleServiceOwner = AComposeLifecycleServiceOwner().apply { 99 | performRestore(null) 100 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 101 | } 102 | composeView!!.setViewTreeLifecycleOwner(composeLifecycleServiceOwner) 103 | composeView!!.setViewTreeSavedStateRegistryOwner(composeLifecycleServiceOwner) 104 | windowManager?.addView(composeView, params) 105 | } 106 | 107 | @Composable 108 | private fun ComposeView.ContentView(params: WindowManager.LayoutParams, windowManager: WindowManager?) { 109 | val logList by aLogViewViewModel.logList.collectAsState() 110 | val logViewVisible by aLogViewViewModel.logViewVisible.asRemember() 111 | ALogScreen( 112 | onClickViewStateChang = { 113 | aLogViewViewModel.changeLogViewVisible() 114 | }, 115 | onClickClose = { 116 | aLogViewViewModel.close() 117 | stopSelf() 118 | }, 119 | dragEvent = { x, y -> 120 | aLogViewViewModel.drag(x, y) 121 | }, 122 | logViewVisible = logViewVisible, 123 | logMessageView = { 124 | ALogMessageScreen(items = logList) 125 | } 126 | ) 127 | 128 | val dragItem by aLogViewViewModel.dragItem.asRemember() 129 | onDrag(dragItem, params, windowManager) 130 | } 131 | 132 | private fun ComposeView.onDrag( 133 | dragItem: ADragItem, 134 | params: WindowManager.LayoutParams, 135 | windowManager: WindowManager?, 136 | ) { 137 | params.x += dragItem.x 138 | params.y += dragItem.y 139 | windowManager?.updateViewLayout(this, params) 140 | } 141 | 142 | @Composable 143 | private fun MutableState.asRemember(): State { 144 | return remember { this } 145 | } 146 | 147 | companion object { 148 | 149 | fun newInstance(context: Context): Intent = 150 | Intent(context, ALogViewService::class.java) 151 | } 152 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/ALogViewViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view 2 | 3 | import android.util.Log 4 | import androidx.annotation.VisibleForTesting 5 | import androidx.compose.runtime.mutableStateOf 6 | import kotlinx.collections.immutable.ImmutableList 7 | import kotlinx.collections.immutable.persistentListOf 8 | import kotlinx.collections.immutable.toPersistentList 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.SupervisorJob 11 | import kotlinx.coroutines.cancel 12 | import kotlinx.coroutines.channels.BufferOverflow 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.MutableSharedFlow 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.asFlow 17 | import kotlinx.coroutines.flow.cancellable 18 | import kotlinx.coroutines.flow.filter 19 | import kotlinx.coroutines.flow.flowOn 20 | import kotlinx.coroutines.flow.launchIn 21 | import kotlinx.coroutines.flow.map 22 | import kotlinx.coroutines.flow.onEach 23 | import kotlinx.coroutines.flow.toList 24 | import tech.thdev.android.log.repository.api.ALogRepository 25 | import tech.thdev.android.log.view.compose.ui.theme.ALogColors 26 | import tech.thdev.android.log.view.coroutines.ADispatcherProvider 27 | import tech.thdev.android.log.view.model.ADragItem 28 | import tech.thdev.android.log.view.model.ALogItem 29 | 30 | class ALogViewViewModel( 31 | private val aLogRepository: ALogRepository, 32 | private val dispatcherProvider: ADispatcherProvider, 33 | ) { 34 | 35 | val logList = MutableStateFlow>(persistentListOf()) 36 | 37 | val logViewVisible = mutableStateOf(true) 38 | 39 | val dragItem = mutableStateOf(ADragItem(50, 50)) 40 | 41 | @VisibleForTesting 42 | val flowDrag = MutableSharedFlow( 43 | extraBufferCapacity = 1, 44 | onBufferOverflow = BufferOverflow.DROP_OLDEST, 45 | ) 46 | 47 | @VisibleForTesting 48 | val flowChangeLogViewVisible = MutableSharedFlow( 49 | extraBufferCapacity = 1, 50 | onBufferOverflow = BufferOverflow.DROP_OLDEST, 51 | ) 52 | 53 | private val coroutineScope by lazy { 54 | CoroutineScope(dispatcherProvider.default() + SupervisorJob()) 55 | } 56 | 57 | fun run() { 58 | aLogRepository.runLog() 59 | flowRun().launchIn(coroutineScope) 60 | flowChangeLogViewVisible().launchIn(coroutineScope) 61 | flowDrag().launchIn(coroutineScope) 62 | } 63 | 64 | fun flowRun(): Flow> = 65 | aLogRepository.flowLog() 66 | .map { 67 | it.asFlow().cancellable() 68 | .map { item -> 69 | val color = when (item.priority) { 70 | Log.DEBUG -> ALogColors.DEBUG 71 | Log.ERROR -> ALogColors.ERROR 72 | Log.WARN -> ALogColors.WARN 73 | Log.INFO -> ALogColors.INFO 74 | else -> ALogColors.DEFAULT 75 | } 76 | ALogItem( 77 | message = item.message, 78 | color = color, 79 | ) 80 | } 81 | .toList() 82 | } 83 | .onEach { list -> 84 | logList.value = list.toPersistentList() 85 | } 86 | .flowOn(dispatcherProvider.main()) 87 | 88 | fun flowChangeLogViewVisible(): Flow = 89 | flowChangeLogViewVisible 90 | .filter { it } 91 | .map { 92 | Log.i("TEMP", "change ${logViewVisible.value}") 93 | logViewVisible.value.not() 94 | } 95 | .onEach { 96 | Log.i("TEMP", "change value ${logViewVisible.value}") 97 | logViewVisible.value = it 98 | } 99 | .flowOn(dispatcherProvider.main()) 100 | 101 | fun flowDrag(): Flow = 102 | flowDrag 103 | .onEach { 104 | dragItem.value = it 105 | } 106 | .flowOn(dispatcherProvider.main()) 107 | 108 | fun changeLogViewVisible() { 109 | Log.i("TEMP", "onClickViewStateChang ${logViewVisible.value}") 110 | flowChangeLogViewVisible.tryEmit(true) 111 | } 112 | 113 | fun drag(x: Int, y: Int) { 114 | flowDrag.tryEmit(ADragItem(x = x, y = y)) 115 | } 116 | 117 | fun close() { 118 | aLogRepository.stopLog() 119 | coroutineScope.cancel() 120 | } 121 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/compose/ALogMessageScreen.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.compose 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.LazyListState 10 | import androidx.compose.foundation.lazy.itemsIndexed 11 | import androidx.compose.foundation.lazy.rememberLazyListState 12 | import androidx.compose.material.Divider 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import kotlinx.collections.immutable.ImmutableList 23 | import kotlinx.collections.immutable.persistentListOf 24 | import kotlinx.coroutines.delay 25 | import tech.thdev.android.log.view.compose.ui.theme.ALogColors 26 | import tech.thdev.android.log.view.model.ALogItem 27 | 28 | @Composable 29 | internal fun ALogMessageScreen( 30 | items: ImmutableList, 31 | ) { 32 | val autoScrollDistance by remember { mutableStateOf(30) } 33 | val scrollState = rememberLazyListState() 34 | 35 | LaunchedEffect(items.size) { 36 | val count = scrollState.layoutInfo.totalItemsCount 37 | 38 | if (count > 0) { 39 | delay(200) 40 | scrollState.scrollToItem(items.lastIndex) 41 | } 42 | 43 | if (items.isNotEmpty()) { 44 | scrollState.canAutoScroll(autoScrollDistance) { 45 | scrollState.scrollToItem(it) 46 | } 47 | } 48 | } 49 | 50 | LazyColumn( 51 | state = scrollState, 52 | modifier = Modifier 53 | .fillMaxWidth() 54 | .height(170.dp) 55 | .padding(2.dp) 56 | .background(color = ALogColors.WHITE) 57 | ) { 58 | itemsIndexed(items) { _, item -> 59 | Column { 60 | Text( 61 | text = item.message, 62 | color = item.color, 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .padding(5.dp) 66 | ) 67 | 68 | Divider() 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Preview 75 | @Composable 76 | internal fun PreviewALogMessageScreen() { 77 | ALogMessageScreen( 78 | items = persistentListOf( 79 | ALogItem( 80 | message = "Log", 81 | color = ALogColors.INFO, 82 | ), 83 | ALogItem( 84 | message = "Error Log", 85 | color = ALogColors.ERROR, 86 | ), 87 | ) 88 | ) 89 | } 90 | 91 | /** 92 | * find scroll position is end 93 | * @param action return last item position 94 | */ 95 | inline fun LazyListState.canAutoScroll(distance: Int, action: (Int) -> Unit) { 96 | val visibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return 97 | val totalCount = layoutInfo.totalItemsCount 98 | if (visibleItemIndex >= totalCount - distance && visibleItemIndex < totalCount) { 99 | action(totalCount - 1) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/compose/ALogScreen.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.compose 2 | 3 | import android.view.MotionEvent 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.IconButton 14 | import androidx.compose.material.Surface 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.ExperimentalComposeUiApi 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.input.pointer.pointerInteropFilter 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import androidx.constraintlayout.compose.ConstraintLayout 25 | import androidx.constraintlayout.compose.Visibility 26 | import kotlinx.collections.immutable.persistentListOf 27 | import tech.thdev.android.log.view.R 28 | import tech.thdev.android.log.view.compose.ui.theme.ALogColors 29 | import tech.thdev.android.log.view.model.ALogItem 30 | 31 | var prevX = 0.0f 32 | var prevY = 0.0f 33 | var isDrag = false 34 | 35 | @OptIn(ExperimentalComposeUiApi::class) 36 | @Composable 37 | internal fun ALogScreen( 38 | onClickViewStateChang: () -> Unit, 39 | onClickClose: () -> Unit, 40 | dragEvent: (x: Int, y: Int) -> Unit, 41 | logViewVisible: Boolean, 42 | logMessageView: @Composable () -> Unit, 43 | ) { 44 | ConstraintLayout { 45 | val (icon, container) = createRefs() 46 | 47 | Surface( 48 | color = ALogColors.BLACK.copy(alpha = 0.5f), 49 | modifier = Modifier 50 | .width(230.dp) 51 | .constrainAs(container) { 52 | linkTo(start = parent.start, end = parent.end) 53 | linkTo(top = parent.top, bottom = parent.bottom) 54 | visibility = Visibility.Visible.takeIf { logViewVisible } ?: Visibility.Gone 55 | } 56 | ) { 57 | Column( 58 | modifier = Modifier 59 | .padding(3.dp) 60 | .background(ALogColors.WHITE.copy(alpha = 0.7f)) 61 | ) { 62 | Row( 63 | verticalAlignment = Alignment.CenterVertically, 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .background(color = ALogColors.WHITE) 67 | ) { 68 | Text( 69 | text = "Log view", 70 | modifier = Modifier 71 | .weight(1f) 72 | .padding(start = 10.dp, end = 10.dp) 73 | ) 74 | 75 | IconButton( 76 | onClick = { onClickClose() }, 77 | modifier = Modifier 78 | .size(32.dp) 79 | ) { 80 | Icon( 81 | painter = painterResource(id = R.drawable.baseline_cancel_24), 82 | contentDescription = null, 83 | ) 84 | } 85 | } 86 | logMessageView() 87 | } 88 | } 89 | 90 | Box( 91 | modifier = Modifier 92 | .pointerInteropFilter { 93 | when (it.action) { 94 | MotionEvent.ACTION_DOWN -> { 95 | isDrag = false 96 | 97 | prevX = it.rawX 98 | prevY = it.rawY 99 | 100 | true 101 | } 102 | 103 | MotionEvent.ACTION_MOVE -> { 104 | val rawX = it.rawX 105 | val rawY = it.rawY 106 | 107 | val newX = rawX - prevX 108 | val newY = rawY - prevY 109 | 110 | dragEvent(newX.toInt(), newY.toInt()) 111 | 112 | if (isDrag.not()) { 113 | isDrag = (newX > 2f || newX < -2f) && (newY > 2f || newY < -2f) 114 | } 115 | 116 | prevX = rawX 117 | prevY = rawY 118 | true 119 | } 120 | 121 | MotionEvent.ACTION_UP -> { 122 | false 123 | } 124 | 125 | else -> false 126 | } 127 | } 128 | .constrainAs(icon) { 129 | linkTo(start = container.start, end = container.start) 130 | linkTo(top = container.top, bottom = container.top) 131 | } 132 | ) { 133 | IconButton( 134 | onClick = { onClickViewStateChang() }, 135 | ) { 136 | Icon( 137 | painter = painterResource(id = R.drawable.baseline_adb_24), 138 | tint = ALogColors.DEBUG, 139 | contentDescription = null, 140 | modifier = Modifier 141 | .size(46.dp) 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | 148 | @Preview 149 | @Composable 150 | internal fun PreviewALogScreen() { 151 | ALogScreen( 152 | logViewVisible = true, 153 | onClickClose = {}, 154 | onClickViewStateChang = {}, 155 | dragEvent = { _, _ -> }, 156 | logMessageView = { 157 | ALogMessageScreen( 158 | items = persistentListOf( 159 | ALogItem( 160 | message = "Log", 161 | color = ALogColors.INFO, 162 | ), 163 | ALogItem( 164 | message = "Error Log", 165 | color = ALogColors.ERROR, 166 | ), 167 | ) 168 | ) 169 | } 170 | ) 171 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/compose/ui/theme/ALogColors.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.compose.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object ALogColors { 6 | 7 | val ERROR = Color(0xFFFF6B68) 8 | val DEBUG = Color(0xFFABB3F8) 9 | val INFO = Color(0xFFA9D967) 10 | val WARN = Color(0xFFE3934B) 11 | val DEFAULT = Color(0xFF000000) 12 | val BLACK = Color(0xFF000000) 13 | val WHITE = Color(0xFFFFFFFF) 14 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/coroutines/ADispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | interface ADispatcherProvider { 7 | 8 | fun default(): CoroutineDispatcher 9 | 10 | fun io(): CoroutineDispatcher 11 | 12 | fun main(): CoroutineDispatcher 13 | 14 | fun test(): CoroutineDispatcher 15 | 16 | companion object { 17 | operator fun invoke(): ADispatcherProvider = object : ADispatcherProvider { 18 | 19 | override fun default(): CoroutineDispatcher = 20 | Dispatchers.Default 21 | 22 | override fun io(): CoroutineDispatcher = 23 | Dispatchers.IO 24 | 25 | override fun main(): CoroutineDispatcher = 26 | Dispatchers.Main.immediate 27 | 28 | override fun test(): CoroutineDispatcher = 29 | Dispatchers.Unconfined 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/model/ADragItem.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.model 2 | 3 | data class ADragItem( 4 | val x: Int, 5 | val y: Int, 6 | ) 7 | -------------------------------------------------------------------------------- /a-log-view/src/main/java/tech/thdev/android/log/view/model/ALogItem.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | @Immutable 7 | data class ALogItem( 8 | val message: String, 9 | val color: Color, 10 | ) -------------------------------------------------------------------------------- /a-log-view/src/main/res/drawable/baseline_adb_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /a-log-view/src/main/res/drawable/baseline_cancel_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /a-log-view/src/main/res/drawable/baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /a-log-view/src/test/java/tech/thdev/android/log/view/ALogViewViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view 2 | 3 | import android.util.Log 4 | import app.cash.turbine.test 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.flowOf 7 | import kotlinx.coroutines.test.runTest 8 | import org.junit.jupiter.api.Assertions 9 | import org.junit.jupiter.api.Test 10 | import org.mockito.kotlin.mock 11 | import org.mockito.kotlin.verify 12 | import org.mockito.kotlin.whenever 13 | import tech.thdev.android.log.repository.api.ALogRepository 14 | import tech.thdev.android.log.repository.api.model.ALogEntity 15 | import tech.thdev.android.log.view.compose.ui.theme.ALogColors 16 | import tech.thdev.android.log.view.coroutines.MockADispatcherProvider 17 | import tech.thdev.android.log.view.model.ADragItem 18 | import tech.thdev.android.log.view.model.ALogItem 19 | 20 | internal class ALogViewViewModelTest { 21 | 22 | private val aLogRepository = mock() 23 | 24 | private val viewModel = ALogViewViewModel( 25 | aLogRepository = aLogRepository, 26 | dispatcherProvider = MockADispatcherProvider(), 27 | ) 28 | 29 | @Test 30 | fun `test initData`() { 31 | Assertions.assertTrue(viewModel.logList.value.isEmpty()) 32 | Assertions.assertEquals(ADragItem(50, 50), viewModel.dragItem.value) 33 | Assertions.assertTrue(viewModel.logViewVisible.value) 34 | } 35 | 36 | @OptIn(ExperimentalCoroutinesApi::class) 37 | @Test 38 | fun `test flowRun`() = runTest { 39 | val mockItem = listOf( 40 | ALogEntity( 41 | priority = Log.ERROR, 42 | message = "Error message", 43 | tag = null, 44 | t = null, 45 | ) 46 | ) 47 | whenever(aLogRepository.flowLog()).thenReturn(flowOf(mockItem)) 48 | viewModel.flowRun() 49 | .test { 50 | val convertItem = mockItem.map { item -> 51 | ALogItem( 52 | message = item.message, 53 | color = ALogColors.ERROR, 54 | ) 55 | } 56 | Assertions.assertEquals(convertItem, awaitItem()) 57 | Assertions.assertEquals(convertItem, viewModel.logList.value) 58 | verify(aLogRepository).flowLog() 59 | 60 | cancelAndConsumeRemainingEvents() 61 | } 62 | } 63 | 64 | @OptIn(ExperimentalCoroutinesApi::class) 65 | @Test 66 | fun `test flowChangeLogViewVisible`() = runTest { 67 | viewModel.flowChangeLogViewVisible() 68 | .test { 69 | Assertions.assertFalse(awaitItem()) 70 | Assertions.assertFalse(viewModel.logViewVisible.value) 71 | 72 | viewModel.changeLogViewVisible() 73 | Assertions.assertTrue(viewModel.logViewVisible.value) 74 | Assertions.assertTrue(awaitItem()) 75 | 76 | cancelAndConsumeRemainingEvents() 77 | } 78 | } 79 | 80 | @OptIn(ExperimentalCoroutinesApi::class) 81 | @Test 82 | fun `test flowDrag`() = runTest { 83 | viewModel.flowDrag() 84 | .test { 85 | expectNoEvents() 86 | 87 | val change = ADragItem(100, 100) 88 | viewModel.drag(100, 100) 89 | Assertions.assertEquals(change, awaitItem()) 90 | Assertions.assertEquals(change, viewModel.dragItem.value) 91 | 92 | cancelAndConsumeRemainingEvents() 93 | } 94 | } 95 | 96 | @Test 97 | fun `test close`() { 98 | viewModel.close() 99 | verify(aLogRepository).stopLog() 100 | } 101 | } -------------------------------------------------------------------------------- /a-log-view/src/test/java/tech/thdev/android/log/view/coroutines/MockADispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.log.view.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 6 | 7 | @OptIn(ExperimentalCoroutinesApi::class) 8 | class MockADispatcherProvider : ADispatcherProvider { 9 | 10 | internal var testCoroutineDispatcher = UnconfinedTestDispatcher() 11 | 12 | override fun default(): CoroutineDispatcher = 13 | testCoroutineDispatcher 14 | 15 | override fun io(): CoroutineDispatcher = 16 | testCoroutineDispatcher 17 | 18 | override fun main(): CoroutineDispatcher = 19 | testCoroutineDispatcher 20 | 21 | override fun test(): CoroutineDispatcher = 22 | testCoroutineDispatcher 23 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | namespace = "tech.thdev.android.logview.example" 8 | buildToolsVersion = libs.versions.buildToolsVersion.get() 9 | compileSdk = libs.versions.compileSdk.get().toInt() 10 | 11 | defaultConfig { 12 | minSdk = libs.versions.minSdk.get().toInt() 13 | targetSdk = libs.versions.targetSdk.get().toInt() 14 | versionCode = libs.versions.versionCode.get().toInt() 15 | versionName = "${libs.versions.major.get()}.${libs.versions.minor.get()}.${libs.versions.hotfix.get()}" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | 24 | buildTypes { 25 | getByName("debug") { 26 | isMinifyEnabled = false 27 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 28 | } 29 | 30 | getByName("release") { 31 | isMinifyEnabled = false 32 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 33 | } 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_11 38 | targetCompatibility = JavaVersion.VERSION_11 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = "1.8" 43 | } 44 | 45 | buildFeatures { 46 | compose = true 47 | } 48 | 49 | composeOptions { 50 | kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get() 51 | } 52 | 53 | packagingOptions { 54 | resources.excludes.addAll( 55 | listOf( 56 | "META-INF/AL2.0", 57 | "META-INF/LGPL2.1", 58 | ) 59 | ) 60 | } 61 | } 62 | 63 | dependencies { 64 | implementation(libs.kotlin.stdlib) 65 | 66 | implementation(libs.androidx.core) 67 | implementation(libs.google.material) 68 | implementation(libs.timber) 69 | 70 | implementation(libs.compose.activity) 71 | implementation(libs.compose.ui) 72 | implementation(libs.compose.runtime) 73 | implementation(libs.compose.foundation) 74 | implementation(libs.compose.material) 75 | implementation(libs.compose.material3) 76 | implementation(libs.compose.uiToolingPreview) 77 | 78 | implementation(projects.aLogView) 79 | implementation(projects.aLogRepositoryApi) 80 | 81 | debugImplementation(libs.compose.uiTooling) 82 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/android/logview/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.logview.example 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.provider.Settings 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Scaffold 13 | import androidx.compose.ui.Modifier 14 | import tech.thdev.android.log.view.ALogViewService 15 | import tech.thdev.android.logview.example.compose.MainScreen 16 | import tech.thdev.android.logview.example.ui.theme.MainTheme 17 | import timber.log.Timber 18 | 19 | class MainActivity : ComponentActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContent { 24 | MainTheme { 25 | Scaffold( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | ) { 29 | Column( 30 | modifier = Modifier 31 | .padding(it) 32 | ) { 33 | MainScreen { 34 | if (Settings.canDrawOverlays(this@MainActivity).not()) { 35 | startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))) 36 | } else { 37 | startService(ALogViewService.newInstance(this@MainActivity)) 38 | Timber.i("Sample start!!!") 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/android/logview/example/compose/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.logview.example.compose 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import tech.thdev.android.logview.example.ui.theme.MainTheme 18 | import timber.log.Timber 19 | 20 | @Composable 21 | internal fun MainScreen( 22 | onClick: () -> Unit = {}, 23 | ) { 24 | Column( 25 | modifier = Modifier 26 | .fillMaxSize() 27 | .padding(20.dp) 28 | ) { 29 | Button( 30 | onClick = onClick, 31 | modifier = Modifier 32 | .padding(20.dp) 33 | ) { 34 | Text( 35 | text = "LogView start!!", 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | ) 39 | } 40 | 41 | var count by remember { mutableStateOf(0) } 42 | Button( 43 | onClick = { 44 | when (count % 4) { 45 | 1 -> Timber.i("Timber info log $count") 46 | 2 -> Timber.e("Timber error log $count") 47 | 3 -> Timber.w("Timber warn log $count") 48 | else -> Timber.d("Timber debug log $count") 49 | } 50 | count += 1 51 | }, 52 | modifier = Modifier 53 | .padding(20.dp) 54 | ) { 55 | Text( 56 | text = "Log test", 57 | modifier = Modifier 58 | .fillMaxWidth() 59 | ) 60 | } 61 | } 62 | } 63 | 64 | @Preview(showBackground = true) 65 | @Composable 66 | internal fun PreviewMainScreen() { 67 | MainTheme { 68 | MainScreen() 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/android/logview/example/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.logview.example.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/android/logview/example/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.logview.example.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun MainTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/android/logview/example/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.android.logview.example.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/ALogViewApp/7fe3cdee9453137347eea71cc1a56e32529f3550/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ALogView 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |