├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values-ja
│ │ │ │ └── strings.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_proxy_tile.xml
│ │ │ │ ├── ic_adb_tile.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── nagopy
│ │ │ │ └── android
│ │ │ │ └── debugassistant
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── MainUiState.kt
│ │ │ │ │ ├── MainViewModel.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── UiModule.kt
│ │ │ │ └── tile
│ │ │ │ │ ├── AdbTileService.kt
│ │ │ │ │ └── ProxyTileService.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ └── DebugAssistantApplication.kt
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── nagopy
│ │ └── android
│ │ └── debugassistant
│ │ └── ui
│ │ └── main
│ │ └── MainActivityKtTest.kt
├── proguard-rules.pro
└── build.gradle
├── data
└── repository
│ ├── .gitignore
│ ├── consumer-rules.pro
│ ├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── nagopy
│ │ └── android
│ │ └── debugassistant
│ │ └── data
│ │ └── repository
│ │ ├── UserPreferencesRepository.kt
│ │ ├── GlobalSettingsRepository.kt
│ │ ├── RepositoryModule.kt
│ │ └── impl
│ │ ├── GlobalSettingRepositoryImpl.kt
│ │ └── UserPreferencesRepositoryImpl.kt
│ ├── proguard-rules.pro
│ └── build.gradle
├── domain
├── model
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── nagopy
│ │ │ │ └── android
│ │ │ │ └── debugassistant
│ │ │ │ └── domain
│ │ │ │ └── model
│ │ │ │ └── ProxyInfo.kt
│ │ └── test
│ │ │ └── java
│ │ │ └── com
│ │ │ └── nagopy
│ │ │ └── android
│ │ │ └── debugassistant
│ │ │ └── domain
│ │ │ └── model
│ │ │ └── ProxyInfoTest.kt
│ └── build.gradle
└── usecase
│ ├── .gitignore
│ ├── consumer-rules.pro
│ ├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── nagopy
│ │ │ └── android
│ │ │ └── debugassistant
│ │ │ └── domain
│ │ │ └── usecase
│ │ │ ├── DisableAdbUseCase.kt
│ │ │ ├── EnableAdbUseCase.kt
│ │ │ ├── DisableProxyUseCase.kt
│ │ │ ├── GetAdbStatusUseCase.kt
│ │ │ ├── GetProxyStatusUseCase.kt
│ │ │ ├── EnableProxyUseCase.kt
│ │ │ ├── GetPermissionStatusUseCase.kt
│ │ │ ├── GetUserProxyInfoUseCase.kt
│ │ │ ├── PutUserProxyInfoUseCase.kt
│ │ │ ├── interactor
│ │ │ ├── EnableProxyInteractor.kt
│ │ │ ├── EnableAdbInteractor.kt
│ │ │ ├── DisableAdbInteractor.kt
│ │ │ ├── GetUserProxyInfoInteractor.kt
│ │ │ ├── DisableProxyInteractor.kt
│ │ │ ├── GetPermissionStatusInteractor.kt
│ │ │ ├── PutUserProxyInfoInteractor.kt
│ │ │ ├── GetAdbStatusInteractor.kt
│ │ │ └── GetProxyStatusInteractor.kt
│ │ │ └── UseCaseModule.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── nagopy
│ │ └── android
│ │ └── debugassistant
│ │ └── domain
│ │ └── usecase
│ │ └── interactor
│ │ ├── GetUserProxyInfoInteractorTest.kt
│ │ ├── PutUserProxyInfoInteractorTest.kt
│ │ ├── EnableAdbInteractorTest.kt
│ │ ├── DisableAdbInteractorTest.kt
│ │ ├── EnableProxyInteractorTest.kt
│ │ ├── DisableProxyInteractorTest.kt
│ │ ├── GetAdbStatusInteractorTest.kt
│ │ └── GetProxyStatusInteractorTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle
├── .idea
└── .gitignore
├── images
├── features_1.png
├── features_2.png
├── install_1.png
├── install_2.png
└── feature_graphic.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .github
├── dependabot.yml
├── workflows
│ └── android.yml
└── copilot-instructions.md
├── detekt.yml
├── settings.gradle
├── .gitignore
├── .serena
├── memories
│ ├── suggested_commands.md
│ ├── task_completion_checklist.md
│ ├── code_style_conventions.md
│ └── project_overview.md
└── project.yml
├── gradle.properties
├── README.md
├── gradlew.bat
├── gradlew
└── LICENSE
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/repository/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/model/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/usecase/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/usecase/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/repository/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/images/features_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/images/features_1.png
--------------------------------------------------------------------------------
/images/features_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/images/features_2.png
--------------------------------------------------------------------------------
/images/install_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/images/install_1.png
--------------------------------------------------------------------------------
/images/install_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/images/install_2.png
--------------------------------------------------------------------------------
/images/feature_graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/images/feature_graphic.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/75py/DebugAssistant/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/data/repository/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/DisableAdbUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface DisableAdbUseCase {
4 | fun disableAdb(): Boolean
5 | }
6 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/EnableAdbUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface EnableAdbUseCase {
4 |
5 | fun enableAdb(): Boolean
6 | }
7 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/DisableProxyUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface DisableProxyUseCase {
4 | fun disableProxy(): Boolean
5 | }
6 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/GetAdbStatusUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface GetAdbStatusUseCase {
4 |
5 | fun isAdbEnabled(): Boolean
6 | }
7 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/GetProxyStatusUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface GetProxyStatusUseCase {
4 | fun isProxyEnabled(): Boolean
5 | }
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle" # See documentation for possible values
4 | directory: "/" # Location of package manifests
5 | target-branch: "update-dependencies"
6 | schedule:
7 | interval: "weekly"
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/EnableProxyUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface EnableProxyUseCase {
4 | fun enableProxy(host: String, port: String): Boolean
5 | }
6 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/com/nagopy/android/debugassistant/data/repository/UserPreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.data.repository
2 |
3 | interface UserPreferencesRepository {
4 | var proxyHost: String
5 | var proxyPort: String
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/GetPermissionStatusUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | interface GetPermissionStatusUseCase {
4 |
5 | fun isPermissionGranted(permission: String): Boolean
6 | }
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 12 22:29:37 JST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
9 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/GetUserProxyInfoUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
4 |
5 | interface GetUserProxyInfoUseCase {
6 | fun getUserProxyInfo(): ProxyInfo
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/PutUserProxyInfoUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
4 |
5 | interface PutUserProxyInfoUseCase {
6 | fun putUserProxyInfo(proxyInfo: ProxyInfo)
7 | }
8 |
--------------------------------------------------------------------------------
/detekt.yml:
--------------------------------------------------------------------------------
1 | complexity:
2 | LongParameterList:
3 | active: false
4 | TooManyFunctions:
5 | ignorePrivate: true
6 |
7 | naming:
8 | FunctionNaming:
9 | ignoreAnnotated: [ 'Composable' ]
10 |
11 | style:
12 | MagicNumber:
13 | ignorePropertyDeclaration: true
14 | MaxLineLength:
15 | maxLineLength: 140
16 |
--------------------------------------------------------------------------------
/domain/model/src/main/java/com/nagopy/android/debugassistant/domain/model/ProxyInfo.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.model
2 |
3 | data class ProxyInfo(
4 | val host: String,
5 | val port: String
6 | ) {
7 | fun isAvailable(): Boolean {
8 | return host.isNotEmpty() && port.isNotEmpty()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/domain/model/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'org.jetbrains.kotlin.jvm'
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_17
8 | targetCompatibility = JavaVersion.VERSION_17
9 | }
10 |
11 | dependencies {
12 | testImplementation 'junit:junit:4.13.2'
13 | testImplementation 'org.jetbrains.kotlin:kotlin-test'
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/main/MainUiState.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.main
2 |
3 | data class MainUiState(
4 | val isLoading: Boolean,
5 | val proxyHost: String = "",
6 | val proxyPort: String = "",
7 | val isPermissionGranted: Boolean = false,
8 | val isProxyEnabled: Boolean = false,
9 | val isAdbEnabled: Boolean = false,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Debug Assistant
3 | WRITE_SECURE_SETTINGSのパーミッションが許可されていません。\n以下のコマンドを実行してください。
4 | 使い方
5 | オープンソースライセンス
6 | HTTP_PROXYは組織の管理者によって利用されている場合があります。この機能の利用は自己責任でお願いします。
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/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/java/com/nagopy/android/debugassistant/ui/UiModule.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui
2 |
3 | import com.nagopy.android.debugassistant.ui.main.MainViewModel
4 | import org.koin.android.ext.koin.androidApplication
5 | import org.koin.androidx.viewmodel.dsl.viewModel
6 | import org.koin.dsl.module
7 |
8 | val uiModule = module {
9 | viewModel { MainViewModel(androidApplication(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Debug Assistant
3 | WRITE_SECURE_SETTINGS is not granted.\nRun the following command:
4 | How to use
5 | Open source licenses
6 | HTTP_PROXY may be managed by an administrator / organization. Please use this feature at your own risk.
7 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Debug Assistant"
16 | include ':app'
17 | include ':data:repository'
18 | include ':domain:usecase'
19 | include ':domain:model'
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant
2 |
3 | import android.app.KeyguardManager
4 | import android.content.ContentResolver
5 | import org.koin.android.ext.koin.androidApplication
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.dsl.module
8 |
9 | val appModule = module {
10 | single { androidContext().contentResolver }
11 |
12 | single { androidApplication().getSystemService(KeyguardManager::class.java) }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_proxy_tile.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/domain/model/src/test/java/com/nagopy/android/debugassistant/domain/model/ProxyInfoTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.model
2 |
3 | import org.junit.Test
4 | import kotlin.test.assertFalse
5 | import kotlin.test.assertTrue
6 |
7 | class ProxyInfoTest {
8 |
9 | @Test
10 | fun isAvailable() {
11 | assertFalse(ProxyInfo("", "").isAvailable())
12 | assertFalse(ProxyInfo("localhost", "").isAvailable())
13 | assertFalse(ProxyInfo("", "8888").isAvailable())
14 | assertTrue(ProxyInfo("localhost", "8888").isAvailable())
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .idea/.name
17 | .idea/compiler.xml
18 | .idea/gradle.xml
19 | .idea/misc.xml
20 | .idea/vcs.xml
21 | .idea/codeStyles/codeStyleConfig.xml
22 | .idea/codeStyles/Project.xml
23 | .idea/inspectionProfiles/ktlint.xml
24 | .idea/inspectionProfiles/profiles_settings.xml
25 | .idea/deploymentTargetDropDown.xml
26 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/com/nagopy/android/debugassistant/data/repository/GlobalSettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.data.repository
2 |
3 | interface GlobalSettingsRepository {
4 |
5 | fun putString(name: String, value: String?): Boolean
6 |
7 | fun getString(name: String): String?
8 |
9 | fun putInt(name: String, value: Int): Boolean
10 |
11 | fun getInt(name: String, def: Int): Int
12 |
13 | companion object {
14 | const val DISABLE_PROXY_VALUE = ":0"
15 | const val SETTING_ON = 1
16 | const val SETTING_OFF = 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/com/nagopy/android/debugassistant/data/repository/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.data.repository
2 |
3 | import com.nagopy.android.debugassistant.data.repository.impl.GlobalSettingRepositoryImpl
4 | import com.nagopy.android.debugassistant.data.repository.impl.UserPreferencesRepositoryImpl
5 | import org.koin.android.ext.koin.androidApplication
6 | import org.koin.dsl.module
7 |
8 | val repositoryModule = module {
9 | single { GlobalSettingRepositoryImpl(get()) }
10 |
11 | single { UserPreferencesRepositoryImpl(androidApplication().applicationContext) }
12 | }
13 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/EnableProxyInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.EnableProxyUseCase
6 |
7 | class EnableProxyInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository
9 | ) : EnableProxyUseCase {
10 |
11 | override fun enableProxy(host: String, port: String): Boolean {
12 | return globalSettingsRepository.putString(Settings.Global.HTTP_PROXY, "$host:$port")
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/EnableAdbInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.EnableAdbUseCase
6 |
7 | class EnableAdbInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository,
9 | ) : EnableAdbUseCase {
10 |
11 | override fun enableAdb(): Boolean {
12 | return globalSettingsRepository.putInt(
13 | Settings.Global.ADB_ENABLED,
14 | GlobalSettingsRepository.SETTING_ON
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/DisableAdbInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.DisableAdbUseCase
6 |
7 | class DisableAdbInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository
9 | ) : DisableAdbUseCase {
10 |
11 | override fun disableAdb(): Boolean {
12 | return globalSettingsRepository.putInt(
13 | Settings.Global.ADB_ENABLED,
14 | GlobalSettingsRepository.SETTING_OFF
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetUserProxyInfoInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
4 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
5 | import com.nagopy.android.debugassistant.domain.usecase.GetUserProxyInfoUseCase
6 |
7 | class GetUserProxyInfoInteractor(
8 | private val userPreferencesRepository: UserPreferencesRepository
9 | ) : GetUserProxyInfoUseCase {
10 |
11 | override fun getUserProxyInfo(): ProxyInfo {
12 | return ProxyInfo(userPreferencesRepository.proxyHost, userPreferencesRepository.proxyPort)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_adb_tile.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/DisableProxyInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.DisableProxyUseCase
6 |
7 | class DisableProxyInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository
9 | ) : DisableProxyUseCase {
10 |
11 | override fun disableProxy(): Boolean {
12 | return globalSettingsRepository.putString(
13 | Settings.Global.HTTP_PROXY,
14 | GlobalSettingsRepository.DISABLE_PROXY_VALUE
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetPermissionStatusInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.core.content.ContextCompat
6 | import com.nagopy.android.debugassistant.domain.usecase.GetPermissionStatusUseCase
7 |
8 | class GetPermissionStatusInteractor(
9 | private val context: Context
10 | ) : GetPermissionStatusUseCase {
11 | override fun isPermissionGranted(permission: String): Boolean {
12 | return ContextCompat.checkSelfPermission(
13 | context,
14 | permission
15 | ) == PackageManager.PERMISSION_GRANTED
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/PutUserProxyInfoInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
4 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
5 | import com.nagopy.android.debugassistant.domain.usecase.PutUserProxyInfoUseCase
6 |
7 | class PutUserProxyInfoInteractor(
8 | private val userPreferencesRepository: UserPreferencesRepository
9 | ) : PutUserProxyInfoUseCase {
10 |
11 | override fun putUserProxyInfo(proxyInfo: ProxyInfo) {
12 | userPreferencesRepository.proxyHost = proxyInfo.host
13 | userPreferencesRepository.proxyPort = proxyInfo.port
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetAdbStatusInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.GetAdbStatusUseCase
6 |
7 | class GetAdbStatusInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository,
9 | ) : GetAdbStatusUseCase {
10 |
11 | override fun isAdbEnabled(): Boolean {
12 | return globalSettingsRepository.getInt(
13 | Settings.Global.ADB_ENABLED,
14 | GlobalSettingsRepository.SETTING_OFF
15 | ) != GlobalSettingsRepository.SETTING_OFF
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetProxyStatusInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.domain.usecase.GetProxyStatusUseCase
6 |
7 | class GetProxyStatusInteractor(
8 | private val globalSettingsRepository: GlobalSettingsRepository,
9 | ) : GetProxyStatusUseCase {
10 |
11 | override fun isProxyEnabled(): Boolean {
12 | val currentValue = globalSettingsRepository.getString(Settings.Global.HTTP_PROXY)
13 | return !currentValue.isNullOrEmpty() && currentValue != GlobalSettingsRepository.DISABLE_PROXY_VALUE
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/DebugAssistantApplication.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant
2 |
3 | import android.app.Application
4 | import com.nagopy.android.debugassistant.data.repository.repositoryModule
5 | import com.nagopy.android.debugassistant.domain.usecase.domainModule
6 | import com.nagopy.android.debugassistant.ui.uiModule
7 | import org.koin.android.ext.koin.androidContext
8 | import org.koin.core.context.startKoin
9 |
10 | class DebugAssistantApplication : Application() {
11 |
12 | override fun onCreate() {
13 | super.onCreate()
14 |
15 | startKoin {
16 | // androidLogger()
17 | androidContext(this@DebugAssistantApplication)
18 | modules(appModule, uiModule, domainModule, repositoryModule)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/data/repository/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/domain/usecase/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontwarn com.google.errorprone.annotations.Immutable
24 |
--------------------------------------------------------------------------------
/.serena/memories/suggested_commands.md:
--------------------------------------------------------------------------------
1 | # Essential Development Commands
2 |
3 | ## Build Commands
4 | - `./gradlew build` - Full project build
5 | - `./gradlew assembleDebug` - Build debug APK
6 | - `./gradlew assembleRelease` - Build release APK
7 |
8 | ## Testing Commands
9 | - `./gradlew testDebugUnitTest` - Run unit tests
10 | - `./gradlew connectedAndroidTest` - Run instrumentation tests
11 |
12 | ## Code Quality
13 | - `./gradlew detekt` - Run static code analysis
14 | - `./gradlew ktlintCheck` - Check code formatting
15 | - `./gradlew ktlintFormat` - Auto-format code
16 |
17 | ## Gradle Management
18 | - `./gradlew clean` - Clean build artifacts
19 | - `./gradlew --refresh-dependencies` - Refresh dependencies
20 |
21 | ## System Utilities (Linux)
22 | - `git` - Version control
23 | - `find . -name "*.kt"` - Find Kotlin files
24 | - `grep -r "pattern" --include="*.kt"` - Search in Kotlin files
25 | - `ls -la` - List files with details
26 | - `cd path/to/directory` - Change directory
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.theme
2 |
3 | import androidx.compose.material.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 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
29 |
--------------------------------------------------------------------------------
/data/repository/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | compileSdk 36
8 |
9 | defaultConfig {
10 | minSdk 28
11 | targetSdk 36
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_17
25 | targetCompatibility JavaVersion.VERSION_17
26 | }
27 | kotlinOptions {
28 | jvmTarget = '17'
29 | }
30 | namespace 'com.nagopy.android.debugassistant.data.repository'
31 | }
32 |
33 | dependencies {
34 | implementation "io.insert-koin:koin-android:3.5.6"
35 | implementation "androidx.security:security-crypto:1.0.0"
36 | }
--------------------------------------------------------------------------------
/data/repository/src/main/java/com/nagopy/android/debugassistant/data/repository/impl/GlobalSettingRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.data.repository.impl
2 |
3 | import android.content.ContentResolver
4 | import android.provider.Settings
5 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
6 |
7 | internal class GlobalSettingRepositoryImpl(private val resolver: ContentResolver) :
8 | GlobalSettingsRepository {
9 |
10 | override fun putString(name: String, value: String?): Boolean {
11 | return Settings.Global.putString(resolver, name, value)
12 | }
13 |
14 | override fun getString(name: String): String? {
15 | return Settings.Global.getString(resolver, name)
16 | }
17 |
18 | override fun putInt(name: String, value: Int): Boolean {
19 | return Settings.Global.putInt(resolver, name, value)
20 | }
21 |
22 | override fun getInt(name: String, def: Int): Int {
23 | return Settings.Global.getInt(resolver, name, def)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetUserProxyInfoInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
4 | import io.mockk.every
5 | import io.mockk.mockk
6 | import org.junit.Before
7 | import org.junit.Test
8 |
9 | class GetUserProxyInfoInteractorTest {
10 | private lateinit var getUserProxyInfoInteractor: GetUserProxyInfoInteractor
11 | private lateinit var userPreferencesRepository: UserPreferencesRepository
12 |
13 | @Before
14 | fun setUp() {
15 | userPreferencesRepository = mockk(relaxed = true)
16 | getUserProxyInfoInteractor = GetUserProxyInfoInteractor(userPreferencesRepository)
17 | }
18 |
19 | @Test
20 | fun getUserProxyInfo() {
21 |
22 | every { userPreferencesRepository.proxyHost } returns "host"
23 | every { userPreferencesRepository.proxyPort } returns "port"
24 | val result = getUserProxyInfoInteractor.getUserProxyInfo()
25 | kotlin.test.assertEquals("host", result.host)
26 | kotlin.test.assertEquals("port", result.port)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - images/**
8 | - README.md
9 | pull_request:
10 | branches: [ main ]
11 | paths-ignore:
12 | - images/**
13 | - README.md
14 |
15 | jobs:
16 | test:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: set up JDK 17
21 | uses: actions/setup-java@v4
22 | with:
23 | java-version: '17'
24 | distribution: 'temurin'
25 | cache: gradle
26 | - name: Grant execute permission for gradlew
27 | run: chmod +x gradlew
28 | - name: Test with Gradle
29 | run: ./gradlew testDebugUnitTest --stacktrace
30 |
31 | detekt:
32 | runs-on: ubuntu-latest
33 | steps:
34 | - uses: actions/checkout@v4
35 | - name: set up JDK 17
36 | uses: actions/setup-java@v4
37 | with:
38 | java-version: '17'
39 | distribution: 'temurin'
40 | cache: gradle
41 | - name: Grant execute permission for gradlew
42 | run: chmod +x gradlew
43 | - name: detekt with Gradle
44 | run: ./gradlew detekt
45 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/PutUserProxyInfoInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
4 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
5 | import io.mockk.mockk
6 | import io.mockk.verify
7 | import org.junit.Before
8 | import org.junit.Test
9 |
10 | class PutUserProxyInfoInteractorTest {
11 | private lateinit var putUserProxyInfoInteractor: PutUserProxyInfoInteractor
12 | private lateinit var userPreferencesRepository: UserPreferencesRepository
13 |
14 | @Before
15 | fun setUp() {
16 | userPreferencesRepository = mockk(relaxed = true)
17 | putUserProxyInfoInteractor = PutUserProxyInfoInteractor(userPreferencesRepository)
18 | }
19 |
20 | @Test
21 | fun getUserProxyInfo() {
22 | val proxyInfo = ProxyInfo("host", "port")
23 | putUserProxyInfoInteractor.putUserProxyInfo(proxyInfo)
24 | verify {
25 | userPreferencesRepository.proxyHost = "host"
26 | userPreferencesRepository.proxyPort = "port"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/domain/usecase/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | compileSdk 36
8 |
9 | defaultConfig {
10 | minSdk 28
11 | targetSdk 36
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_17
25 | targetCompatibility JavaVersion.VERSION_17
26 | }
27 | kotlinOptions {
28 | jvmTarget = '17'
29 | }
30 | namespace 'com.nagopy.android.debugassistant.domain.usecase'
31 | }
32 |
33 | dependencies {
34 | implementation project(":domain:model")
35 | implementation project(":data:repository")
36 | implementation "io.insert-koin:koin-android:3.5.6"
37 | testImplementation 'junit:junit:4.13.2'
38 | testImplementation "io.mockk:mockk:1.14.0"
39 | testImplementation 'org.jetbrains.kotlin:kotlin-test'
40 | }
--------------------------------------------------------------------------------
/.serena/memories/task_completion_checklist.md:
--------------------------------------------------------------------------------
1 | # Task Completion Checklist
2 |
3 | When completing a development task, follow these steps:
4 |
5 | ## Code Changes
6 | 1. **Make minimal changes**: Only modify what's necessary for the task
7 | 2. **Follow architecture patterns**: Maintain Clean Architecture structure
8 | 3. **Add tests**: Include unit tests for business logic, UI tests for interactions
9 | 4. **Handle errors**: Implement proper error handling for system operations
10 |
11 | ## Build and Quality Checks
12 | 1. **Build the project**: `./gradlew build`
13 | 2. **Run tests**: `./gradlew testDebugUnitTest`
14 | 3. **Check code quality**: `./gradlew detekt`
15 | 4. **Format code**: `./gradlew ktlintCheck` or `./gradlew ktlintFormat`
16 |
17 | ## System Integration
18 | - Verify WRITE_SECURE_SETTINGS permission handling
19 | - Test with system settings modifications
20 | - Ensure Quick Settings tiles work properly
21 |
22 | ## Documentation
23 | - Update documentation if changes affect API or architecture
24 | - Add comments for complex logic (matching existing style)
25 | - Update README if new features are added
26 |
27 | ## Final Verification
28 | - Test on device/emulator with required permissions
29 | - Verify no regression in existing functionality
30 | - Check that all modules compile and tests pass
--------------------------------------------------------------------------------
/.serena/memories/code_style_conventions.md:
--------------------------------------------------------------------------------
1 | # Code Style and Conventions
2 |
3 | ## Naming Conventions
4 | - **Classes**: PascalCase (e.g., `MainActivity`, `EnableProxyUseCase`)
5 | - **Functions**: camelCase (e.g., `enableProxy()`, `onProxySwitchClicked()`)
6 | - **Composables**: PascalCase with `@Composable` annotation
7 | - **Variables**: camelCase (e.g., `isProxyEnabled`, `proxyHost`)
8 | - **Constants**: UPPER_SNAKE_CASE
9 |
10 | ## Architecture Patterns
11 | - **Clean Architecture**: Data/Domain/UI layer separation
12 | - **MVVM**: Model-View-ViewModel with Compose
13 | - **Repository Pattern**: Data access abstraction
14 | - **Use Case/Interactor Pattern**: Business logic encapsulation
15 |
16 | ## Compose UI Patterns
17 | - Use `@Composable` functions for UI components
18 | - State management with `remember` and state hoisting
19 | - Material Design components
20 | - Modifier chaining for styling
21 |
22 | ## Dependency Injection with Koin
23 | ```kotlin
24 | val domainModule = module {
25 | single { EnableProxyInteractor(get()) }
26 | single { GlobalSettingRepositoryImpl(get()) }
27 | }
28 | ```
29 |
30 | ## Error Handling
31 | - Handle SecurityException when accessing system settings
32 | - Provide graceful degradation when permissions not granted
33 | - Use try-catch blocks for system-level operations
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun DebugAssistantTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/.serena/memories/project_overview.md:
--------------------------------------------------------------------------------
1 | # DebugAssistant Project Overview
2 |
3 | ## Purpose
4 | Debug Assistant is an Android application that helps Android developers by providing quick access to development tools through system tiles. The app enables toggling proxy settings and ADB (Android Debug Bridge) directly from the Quick Settings panel.
5 |
6 | ## Tech Stack
7 | - **Language**: Kotlin
8 | - **Min SDK**: 28 (Android 9.0)
9 | - **Current SDK**: compileSdk 35, targetSdk 35
10 | - **Architecture**: Clean Architecture with MVVM
11 | - **UI Framework**: Jetpack Compose 1.8.0
12 | - **Dependency Injection**: Koin 3.5.6
13 | - **Build System**: Gradle with Android Gradle Plugin 8.8.0/8.9.2
14 | - **Testing**: JUnit, MockK, Compose Testing
15 |
16 | ## Project Structure
17 | Multi-module Android project:
18 | - `app/` - Main application module (UI, ViewModels, Activities, Tile Services)
19 | - `data/repository/` - Data layer with repository implementations
20 | - `domain/usecase/` - Business logic and use cases
21 | - `domain/model/` - Domain models and entities
22 |
23 | ## Key Features
24 | 1. Proxy Toggle: Enable/disable system HTTP proxy via Quick Settings
25 | 2. ADB Toggle: Enable/disable Android Debug Bridge via Quick Settings
26 | 3. Permission Management: Handles WRITE_SECURE_SETTINGS permission
27 |
28 | ## Package Structure
29 | Base package: `com.nagopy.android.debugassistant`
30 | - Clear separation between UI, domain, and data layers
31 | - Feature-based packaging within modules
--------------------------------------------------------------------------------
/data/repository/src/main/java/com/nagopy/android/debugassistant/data/repository/impl/UserPreferencesRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.data.repository.impl
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.security.crypto.EncryptedSharedPreferences
6 | import androidx.security.crypto.MasterKeys
7 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
8 |
9 | internal class UserPreferencesRepositoryImpl(context: Context) : UserPreferencesRepository {
10 | private val sharedPreferences: SharedPreferences
11 |
12 | init {
13 | val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
14 | sharedPreferences = EncryptedSharedPreferences.create(
15 | "secret_shared_prefs",
16 | masterKeyAlias,
17 | context,
18 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
19 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
20 | )
21 | }
22 |
23 | override var proxyHost: String
24 | get() = sharedPreferences.getString("proxyHost", "") ?: ""
25 | set(value) {
26 | sharedPreferences.edit().putString("proxyHost", value).apply()
27 | }
28 | override var proxyPort: String
29 | get() = sharedPreferences.getString("proxyPort", "") ?: ""
30 | set(value) {
31 | sharedPreferences.edit().putString("proxyPort", value).apply()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/EnableAdbInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.verify
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | class EnableAdbInteractorTest {
12 |
13 | private lateinit var enableAdbInteractor: EnableAdbInteractor
14 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
15 |
16 | @Before
17 | fun setUp() {
18 | globalSettingsRepository = mockk(relaxed = true)
19 | enableAdbInteractor = EnableAdbInteractor(globalSettingsRepository)
20 | }
21 |
22 | @Test
23 | fun enableAdb_putSuccess() {
24 | every { globalSettingsRepository.putInt(any(), any()) } returns true
25 | val ret = enableAdbInteractor.enableAdb()
26 | kotlin.test.assertTrue(ret)
27 | verify {
28 | globalSettingsRepository.putInt(
29 | Settings.Global.ADB_ENABLED,
30 | GlobalSettingsRepository.SETTING_ON
31 | )
32 | }
33 | }
34 |
35 | @Test
36 | fun enableAdb_putError() {
37 | every { globalSettingsRepository.putInt(any(), any()) } returns false
38 | val ret = enableAdbInteractor.enableAdb()
39 | kotlin.test.assertFalse(ret)
40 | verify {
41 | globalSettingsRepository.putInt(
42 | Settings.Global.ADB_ENABLED,
43 | GlobalSettingsRepository.SETTING_ON
44 | )
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/DisableAdbInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.verify
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | class DisableAdbInteractorTest {
12 |
13 | private lateinit var disableAdbInteractor: DisableAdbInteractor
14 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
15 |
16 | @Before
17 | fun setUp() {
18 | globalSettingsRepository = mockk(relaxed = true)
19 | disableAdbInteractor = DisableAdbInteractor(globalSettingsRepository)
20 | }
21 |
22 | @Test
23 | fun disableAdb_putSuccess() {
24 | every { globalSettingsRepository.putInt(any(), any()) } returns true
25 | val ret = disableAdbInteractor.disableAdb()
26 | kotlin.test.assertTrue(ret)
27 | verify {
28 | globalSettingsRepository.putInt(
29 | Settings.Global.ADB_ENABLED,
30 | GlobalSettingsRepository.SETTING_OFF
31 | )
32 | }
33 | }
34 |
35 | @Test
36 | fun disableAdb_putError() {
37 | every { globalSettingsRepository.putInt(any(), any()) } returns false
38 | val ret = disableAdbInteractor.disableAdb()
39 | kotlin.test.assertFalse(ret)
40 | verify {
41 | globalSettingsRepository.putInt(
42 | Settings.Global.ADB_ENABLED,
43 | GlobalSettingsRepository.SETTING_OFF
44 | )
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/EnableProxyInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.verify
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | class EnableProxyInteractorTest {
12 |
13 | private lateinit var enableProxyInteractor: EnableProxyInteractor
14 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
15 |
16 | @Before
17 | fun setUp() {
18 | globalSettingsRepository = mockk(relaxed = true)
19 | enableProxyInteractor =
20 | EnableProxyInteractor(globalSettingsRepository)
21 | }
22 |
23 | @Test
24 | fun enableProxy_putSuccess() {
25 | val host = "localhost"
26 | val port = "8080"
27 |
28 | every { globalSettingsRepository.putString(any(), any()) } returns true
29 | val ret = enableProxyInteractor.enableProxy(host, port)
30 | kotlin.test.assertTrue(ret)
31 | verify {
32 | globalSettingsRepository.putString(Settings.Global.HTTP_PROXY, "$host:$port")
33 | }
34 | }
35 |
36 | @Test
37 | fun enableProxy_putError() {
38 | val host = "localhost"
39 | val port = "8080"
40 |
41 | every { globalSettingsRepository.putString(any(), any()) } returns false
42 | val ret2 = enableProxyInteractor.enableProxy(host, port)
43 | kotlin.test.assertFalse(ret2)
44 | verify { globalSettingsRepository.putString(Settings.Global.HTTP_PROXY, "$host:$port") }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/DisableProxyInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.verify
8 | import org.junit.Before
9 | import org.junit.Test
10 | import kotlin.test.assertFalse
11 | import kotlin.test.assertTrue
12 |
13 | class DisableProxyInteractorTest {
14 |
15 | private lateinit var disableProxyInteractor: DisableProxyInteractor
16 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
17 |
18 | @Before
19 | fun setUp() {
20 | globalSettingsRepository = mockk(relaxed = true)
21 | disableProxyInteractor =
22 | DisableProxyInteractor(globalSettingsRepository)
23 | }
24 |
25 | @Test
26 | fun disableProxy_putSuccess() {
27 | every { globalSettingsRepository.putString(any(), any()) } returns true
28 | val ret = disableProxyInteractor.disableProxy()
29 | assertTrue(ret)
30 | verify {
31 | globalSettingsRepository.putString(
32 | Settings.Global.HTTP_PROXY,
33 | GlobalSettingsRepository.DISABLE_PROXY_VALUE
34 | )
35 | }
36 | }
37 |
38 | @Test
39 | fun disableProxy_putError() {
40 | every { globalSettingsRepository.putString(any(), any()) } returns false
41 | val ret2 = disableProxyInteractor.disableProxy()
42 | assertFalse(ret2)
43 | verify {
44 | globalSettingsRepository.putString(
45 | Settings.Global.HTTP_PROXY,
46 | GlobalSettingsRepository.DISABLE_PROXY_VALUE
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetAdbStatusInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import io.mockk.clearAllMocks
6 | import io.mockk.every
7 | import io.mockk.mockk
8 | import io.mockk.verify
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | class GetAdbStatusInteractorTest {
13 | private lateinit var getAdbStatusInteractor: GetAdbStatusInteractor
14 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
15 |
16 | @Before
17 | fun setUp() {
18 | globalSettingsRepository = mockk(relaxed = true)
19 | getAdbStatusInteractor =
20 | GetAdbStatusInteractor(globalSettingsRepository)
21 | }
22 |
23 | @Test
24 | fun isProxyEnabled() {
25 | every {
26 | globalSettingsRepository.getInt(
27 | any(),
28 | any()
29 | )
30 | } returns GlobalSettingsRepository.SETTING_OFF
31 | kotlin.test.assertFalse(getAdbStatusInteractor.isAdbEnabled())
32 | verify {
33 | globalSettingsRepository.getInt(
34 | Settings.Global.ADB_ENABLED,
35 | GlobalSettingsRepository.SETTING_OFF
36 | )
37 | }
38 |
39 | clearAllMocks()
40 |
41 | every {
42 | globalSettingsRepository.getInt(
43 | any(),
44 | any()
45 | )
46 | } returns GlobalSettingsRepository.SETTING_ON
47 | kotlin.test.assertTrue(getAdbStatusInteractor.isAdbEnabled())
48 | verify {
49 | globalSettingsRepository.getInt(
50 | Settings.Global.ADB_ENABLED,
51 | GlobalSettingsRepository.SETTING_OFF
52 | )
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/domain/usecase/src/test/java/com/nagopy/android/debugassistant/domain/usecase/interactor/GetProxyStatusInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase.interactor
2 |
3 | import android.provider.Settings
4 | import com.nagopy.android.debugassistant.data.repository.GlobalSettingsRepository
5 | import com.nagopy.android.debugassistant.data.repository.UserPreferencesRepository
6 | import io.mockk.every
7 | import io.mockk.mockk
8 | import io.mockk.verify
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | class GetProxyStatusInteractorTest {
13 | private lateinit var getProxyStatusInteractor: GetProxyStatusInteractor
14 | private lateinit var globalSettingsRepository: GlobalSettingsRepository
15 | private lateinit var userPreferencesRepository: UserPreferencesRepository
16 |
17 | @Before
18 | fun setUp() {
19 | globalSettingsRepository = mockk(relaxed = true)
20 | userPreferencesRepository = mockk(relaxed = true)
21 | getProxyStatusInteractor =
22 | GetProxyStatusInteractor(globalSettingsRepository)
23 | }
24 |
25 | @Test
26 | fun isProxyEnabled() {
27 | every { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) } returns null
28 | kotlin.test.assertFalse(getProxyStatusInteractor.isProxyEnabled())
29 | verify { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) }
30 |
31 | every { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) } returns ""
32 | kotlin.test.assertFalse(getProxyStatusInteractor.isProxyEnabled())
33 | verify { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) }
34 |
35 | every { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) } returns GlobalSettingsRepository.DISABLE_PROXY_VALUE
36 | kotlin.test.assertFalse(getProxyStatusInteractor.isProxyEnabled())
37 | verify { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) }
38 |
39 | every { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) } returns "localhost:8080"
40 | kotlin.test.assertTrue(getProxyStatusInteractor.isProxyEnabled())
41 | verify { globalSettingsRepository.getString(Settings.Global.HTTP_PROXY) }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/com/nagopy/android/debugassistant/domain/usecase/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.domain.usecase
2 |
3 | import com.nagopy.android.debugassistant.domain.usecase.DisableAdbUseCase
4 | import com.nagopy.android.debugassistant.domain.usecase.DisableProxyUseCase
5 | import com.nagopy.android.debugassistant.domain.usecase.EnableAdbUseCase
6 | import com.nagopy.android.debugassistant.domain.usecase.EnableProxyUseCase
7 | import com.nagopy.android.debugassistant.domain.usecase.GetAdbStatusUseCase
8 | import com.nagopy.android.debugassistant.domain.usecase.GetPermissionStatusUseCase
9 | import com.nagopy.android.debugassistant.domain.usecase.GetProxyStatusUseCase
10 | import com.nagopy.android.debugassistant.domain.usecase.GetUserProxyInfoUseCase
11 | import com.nagopy.android.debugassistant.domain.usecase.PutUserProxyInfoUseCase
12 | import com.nagopy.android.debugassistant.domain.usecase.interactor.DisableAdbInteractor
13 | import com.nagopy.android.debugassistant.domain.usecase.interactor.DisableProxyInteractor
14 | import com.nagopy.android.debugassistant.domain.usecase.interactor.EnableAdbInteractor
15 | import com.nagopy.android.debugassistant.domain.usecase.interactor.EnableProxyInteractor
16 | import com.nagopy.android.debugassistant.domain.usecase.interactor.GetAdbStatusInteractor
17 | import com.nagopy.android.debugassistant.domain.usecase.interactor.GetPermissionStatusInteractor
18 | import com.nagopy.android.debugassistant.domain.usecase.interactor.GetProxyStatusInteractor
19 | import com.nagopy.android.debugassistant.domain.usecase.interactor.GetUserProxyInfoInteractor
20 | import com.nagopy.android.debugassistant.domain.usecase.interactor.PutUserProxyInfoInteractor
21 | import org.koin.dsl.module
22 |
23 | val domainModule = module {
24 |
25 | single { GetProxyStatusInteractor(get()) }
26 |
27 | single { EnableProxyInteractor(get()) }
28 |
29 | single { DisableProxyInteractor(get()) }
30 |
31 | single { GetAdbStatusInteractor(get()) }
32 |
33 | single { EnableAdbInteractor(get()) }
34 |
35 | single { DisableAdbInteractor(get()) }
36 |
37 | single { GetPermissionStatusInteractor(get()) }
38 |
39 | single { GetUserProxyInfoInteractor(get()) }
40 |
41 | single { PutUserProxyInfoInteractor(get()) }
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Debug Assistant
2 |
3 | Assists Android Developers!
4 |
5 | ## Features
6 |
7 | * Proxy toggle in quick settings
8 | * Adb toggle in quick settings
9 |
10 |
11 |
12 | ## Install
13 |
14 | 1. Download and install from [Google Play](https://play.google.com/store/apps/details?id=com.nagopy.android.debugassistant)
15 | 2. Grant WRITE_SECURE_SETTINGS permission by following command
16 | `adb shell pm grant com.nagopy.android.debugassistant android.permission.WRITE_SECURE_SETTINGS`
17 | 3. Input your proxy settings
18 | 4. Add quick setting icons
19 |
20 |
21 |
22 | ## Architecture
23 |
24 | ```
25 | debugassistant
26 | ├─data
27 | │ └─repository
28 | │ │ GlobalSettingsRepository.kt
29 | │ │ UserPreferencesRepository.kt
30 | │ │ ...
31 | │ └─impl
32 | │ GlobalSettingRepositoryImpl.kt
33 | │ UserPreferencesRepositoryImpl.kt
34 | │ ...
35 | ├─domain
36 | │ │ ProxyInfo.kt
37 | │ │ ...
38 | │ └─usecase
39 | │ │ EnableProxyUseCase.kt
40 | │ │ DisableProxyUseCase.kt
41 | │ │ ...
42 | │ └─interactor
43 | │ EnableProxyInteractor.kt
44 | │ DisableProxyInteractor.kt
45 | │ ...
46 | └─ui
47 | ├─main
48 | │ MainActivity.kt
49 | │ MainViewModel.kt
50 | │ MainUiState.kt
51 | │ ...
52 | └─tile
53 | ProxyTileService.kt
54 | ...
55 | ```
56 |
57 | ```mermaid
58 | classDiagram
59 |
60 | MainActivity --> MainViewModel : events
61 | MainViewModel --> MainActivity : MainUiState
62 | MainViewModel ..> EnableProxyUseCase
63 | MainViewModel ..> DisableProxyUseCase
64 | EnableProxyUseCase <|.. EnableProxyInteractor : implements
65 | DisableProxyUseCase <|.. DisableProxyInteractor : implements
66 |
67 | EnableProxyInteractor ..> GlobalSettingsRepository
68 | DisableProxyInteractor ..> GlobalSettingsRepository
69 |
70 | GlobalSettingsRepository <|.. GlobalSettingsRepositoryImpl : implements
71 | ```
72 |
73 | ## License
74 |
75 | ```
76 | Copyright 2022 75py
77 |
78 | Licensed under the Apache License, Version 2.0 (the "License");
79 | you may not use this file except in compliance with the License.
80 | You may obtain a copy of the License at
81 |
82 | http://www.apache.org/licenses/LICENSE-2.0
83 |
84 | Unless required by applicable law or agreed to in writing, software
85 | distributed under the License is distributed on an "AS IS" BASIS,
86 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
87 | See the License for the specific language governing permissions and
88 | limitations under the License.
89 | ```
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/tile/AdbTileService.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.tile
2 |
3 | import android.Manifest
4 | import android.app.KeyguardManager
5 | import android.database.ContentObserver
6 | import android.net.Uri
7 | import android.os.Handler
8 | import android.os.Looper
9 | import android.provider.Settings
10 | import android.service.quicksettings.Tile
11 | import android.service.quicksettings.TileService
12 | import com.nagopy.android.debugassistant.domain.usecase.DisableAdbUseCase
13 | import com.nagopy.android.debugassistant.domain.usecase.EnableAdbUseCase
14 | import com.nagopy.android.debugassistant.domain.usecase.GetAdbStatusUseCase
15 | import com.nagopy.android.debugassistant.domain.usecase.GetPermissionStatusUseCase
16 | import org.koin.android.ext.android.inject
17 |
18 | class AdbTileService : TileService() {
19 |
20 | private val getPermissionStatusUseCase: GetPermissionStatusUseCase by inject()
21 | private val getAdbStatusUseCase: GetAdbStatusUseCase by inject()
22 | private val enableAdbUseCase: EnableAdbUseCase by inject()
23 | private val disableAdbUseCase: DisableAdbUseCase by inject()
24 | private val keyguardManager: KeyguardManager by inject()
25 |
26 | private fun refresh() {
27 | qsTile.state =
28 | if (getPermissionStatusUseCase.isPermissionGranted(Manifest.permission.WRITE_SECURE_SETTINGS)) {
29 | if (getAdbStatusUseCase.isAdbEnabled()) {
30 | Tile.STATE_ACTIVE
31 | } else {
32 | Tile.STATE_INACTIVE
33 | }
34 | } else {
35 | Tile.STATE_UNAVAILABLE
36 | }
37 | qsTile.updateTile()
38 | }
39 |
40 | override fun onStartListening() {
41 | super.onStartListening()
42 | refresh()
43 | contentResolver.registerContentObserver(
44 | Settings.Global.getUriFor(Settings.Global.ADB_ENABLED),
45 | false,
46 | settingsObserver
47 | )
48 | }
49 |
50 | override fun onStopListening() {
51 | super.onStopListening()
52 | contentResolver.unregisterContentObserver(settingsObserver)
53 | }
54 |
55 | override fun onClick() {
56 | super.onClick()
57 | when (qsTile.state) {
58 | Tile.STATE_ACTIVE -> {
59 | disableAdbUseCase.disableAdb()
60 | }
61 | Tile.STATE_INACTIVE -> {
62 | if (!keyguardManager.isKeyguardLocked) {
63 | enableAdbUseCase.enableAdb()
64 | }
65 | }
66 | }
67 | refresh()
68 | }
69 |
70 | private val handler: Handler = Handler(Looper.getMainLooper())
71 | private val settingsObserver: ContentObserver = object : ContentObserver(handler) {
72 | override fun onChange(selfChange: Boolean, uri: Uri?) {
73 | refresh()
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.compose'
5 | id 'org.jlleitschuh.gradle.ktlint'
6 | id 'com.google.android.gms.oss-licenses-plugin'
7 | id 'io.gitlab.arturbosch.detekt'
8 | }
9 |
10 | detekt {
11 | buildUponDefaultConfig = true
12 | config = files("$projectDir/../detekt.yml")
13 | }
14 |
15 |
16 | android {
17 | compileSdk 36
18 |
19 | defaultConfig {
20 | applicationId "com.nagopy.android.debugassistant"
21 | minSdk 28
22 | targetSdk 36
23 | versionCode 9
24 | versionName "1.1.3"
25 |
26 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
27 | vectorDrawables {
28 | useSupportLibrary true
29 | }
30 | }
31 |
32 | buildTypes {
33 | release {
34 | minifyEnabled true
35 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
36 | }
37 | }
38 | compileOptions {
39 | sourceCompatibility JavaVersion.VERSION_17
40 | targetCompatibility JavaVersion.VERSION_17
41 | }
42 | kotlinOptions {
43 | jvmTarget = '17'
44 | }
45 | buildFeatures {
46 | compose true
47 | buildConfig true
48 | }
49 | composeOptions {
50 | kotlinCompilerExtensionVersion compose_version
51 | }
52 | packagingOptions {
53 | resources {
54 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
55 | }
56 | }
57 | namespace 'com.nagopy.android.debugassistant'
58 | testNamespace 'com.nagopy.android.debugassistant.androidtest'
59 | }
60 |
61 | dependencies {
62 | implementation project(":domain:model")
63 | implementation project(":domain:usecase")
64 | implementation project(":data:repository")
65 |
66 | implementation 'androidx.core:core-ktx:1.16.0'
67 | implementation "androidx.compose.ui:ui:$compose_version"
68 | implementation "androidx.compose.material:material:$compose_version"
69 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
70 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.9.2'
71 | implementation 'androidx.activity:activity-compose:1.10.1'
72 |
73 | testImplementation 'junit:junit:4.13.2'
74 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
75 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
76 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
77 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
78 |
79 | testImplementation "io.mockk:mockk:1.14.0"
80 | testImplementation 'org.jetbrains.kotlin:kotlin-test'
81 |
82 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
83 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
84 | androidTestImplementation 'org.jetbrains.kotlin:kotlin-test'
85 |
86 | androidTestImplementation "io.mockk:mockk-android:1.14.0"
87 |
88 | implementation "io.insert-koin:koin-android:3.5.6"
89 |
90 | implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
91 | implementation "androidx.appcompat:appcompat:1.7.0"
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/tile/ProxyTileService.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.tile
2 |
3 | import android.Manifest
4 | import android.app.KeyguardManager
5 | import android.database.ContentObserver
6 | import android.net.Uri
7 | import android.os.Handler
8 | import android.os.Looper
9 | import android.provider.Settings
10 | import android.service.quicksettings.Tile
11 | import android.service.quicksettings.TileService
12 | import com.nagopy.android.debugassistant.domain.usecase.DisableProxyUseCase
13 | import com.nagopy.android.debugassistant.domain.usecase.EnableProxyUseCase
14 | import com.nagopy.android.debugassistant.domain.usecase.GetPermissionStatusUseCase
15 | import com.nagopy.android.debugassistant.domain.usecase.GetProxyStatusUseCase
16 | import com.nagopy.android.debugassistant.domain.usecase.GetUserProxyInfoUseCase
17 | import org.koin.android.ext.android.inject
18 |
19 | class ProxyTileService : TileService() {
20 |
21 | private val getPermissionStatusUseCase: GetPermissionStatusUseCase by inject()
22 | private val getProxyStatusUseCase: GetProxyStatusUseCase by inject()
23 | private val enableProUseCase: EnableProxyUseCase by inject()
24 | private val disableProxyUseCase: DisableProxyUseCase by inject()
25 | private val getUserProxyInfoUseCase: GetUserProxyInfoUseCase by inject()
26 | private val keyguardManager: KeyguardManager by inject()
27 |
28 | private fun refresh() {
29 | qsTile.state =
30 | if (getPermissionStatusUseCase.isPermissionGranted(Manifest.permission.WRITE_SECURE_SETTINGS) &&
31 | getUserProxyInfoUseCase.getUserProxyInfo().isAvailable()
32 | ) {
33 | if (getProxyStatusUseCase.isProxyEnabled()) {
34 | Tile.STATE_ACTIVE
35 | } else {
36 | Tile.STATE_INACTIVE
37 | }
38 | } else {
39 | Tile.STATE_UNAVAILABLE
40 | }
41 | qsTile.updateTile()
42 | }
43 |
44 | override fun onStartListening() {
45 | super.onStartListening()
46 | refresh()
47 | contentResolver.registerContentObserver(
48 | Settings.Global.getUriFor(Settings.Global.HTTP_PROXY),
49 | false,
50 | settingsObserver
51 | )
52 | }
53 |
54 | override fun onStopListening() {
55 | super.onStopListening()
56 | contentResolver.unregisterContentObserver(settingsObserver)
57 | }
58 |
59 | override fun onClick() {
60 | super.onClick()
61 | when (qsTile.state) {
62 | Tile.STATE_ACTIVE -> {
63 | disableProxyUseCase.disableProxy()
64 | }
65 | Tile.STATE_INACTIVE -> {
66 | if (!keyguardManager.isKeyguardLocked) {
67 | val proxyInfo = getUserProxyInfoUseCase.getUserProxyInfo()
68 | enableProUseCase.enableProxy(
69 | proxyInfo.host,
70 | proxyInfo.port
71 | )
72 | }
73 | }
74 | }
75 | refresh()
76 | }
77 |
78 | private val handler: Handler = Handler(Looper.getMainLooper())
79 | private val settingsObserver: ContentObserver = object : ContentObserver(handler) {
80 | override fun onChange(selfChange: Boolean, uri: Uri?) {
81 | refresh()
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/.serena/project.yml:
--------------------------------------------------------------------------------
1 | # language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
2 | # * For C, use cpp
3 | # * For JavaScript, use typescript
4 | # Special requirements:
5 | # * csharp: Requires the presence of a .sln file in the project folder.
6 | language: kotlin
7 |
8 | # whether to use the project's gitignore file to ignore files
9 | # Added on 2025-04-07
10 | ignore_all_files_in_gitignore: true
11 | # list of additional paths to ignore
12 | # same syntax as gitignore, so you can use * and **
13 | # Was previously called `ignored_dirs`, please update your config if you are using that.
14 | # Added (renamed) on 2025-04-07
15 | ignored_paths: []
16 |
17 | # whether the project is in read-only mode
18 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error
19 | # Added on 2025-04-18
20 | read_only: false
21 |
22 |
23 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
24 | # Below is the complete list of tools for convenience.
25 | # To make sure you have the latest list of tools, and to view their descriptions,
26 | # execute `uv run scripts/print_tool_overview.py`.
27 | #
28 | # * `activate_project`: Activates a project by name.
29 | # * `check_onboarding_performed`: Checks whether project onboarding was already performed.
30 | # * `create_text_file`: Creates/overwrites a file in the project directory.
31 | # * `delete_lines`: Deletes a range of lines within a file.
32 | # * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
33 | # * `execute_shell_command`: Executes a shell command.
34 | # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
35 | # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
36 | # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
37 | # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
38 | # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
39 | # * `initial_instructions`: Gets the initial instructions for the current project.
40 | # Should only be used in settings where the system prompt cannot be set,
41 | # e.g. in clients you have no control over, like Claude Desktop.
42 | # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
43 | # * `insert_at_line`: Inserts content at a given line in a file.
44 | # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
45 | # * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
46 | # * `list_memories`: Lists memories in Serena's project-specific memory store.
47 | # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
48 | # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
49 | # * `read_file`: Reads a file within the project directory.
50 | # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
51 | # * `remove_project`: Removes a project from the Serena configuration.
52 | # * `replace_lines`: Replaces a range of lines within a file with new content.
53 | # * `replace_symbol_body`: Replaces the full definition of a symbol.
54 | # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
55 | # * `search_for_pattern`: Performs a search for a pattern in the project.
56 | # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
57 | # * `switch_modes`: Activates modes by providing a list of their names
58 | # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
59 | # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
60 | # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
61 | # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
62 | excluded_tools: []
63 |
64 | # initial prompt for the project. It will always be given to the LLM upon activating the project
65 | # (contrary to the memories, which are loaded on demand).
66 | initial_prompt: ""
67 |
68 | project_name: "DebugAssistant"
69 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/nagopy/android/debugassistant/ui/main/MainActivityKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.main
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.platform.testTag
7 | import androidx.compose.ui.test.assertIsDisplayed
8 | import androidx.compose.ui.test.assertIsEnabled
9 | import androidx.compose.ui.test.assertIsNotEnabled
10 | import androidx.compose.ui.test.assertIsOff
11 | import androidx.compose.ui.test.assertIsOn
12 | import androidx.compose.ui.test.assertTextEquals
13 | import androidx.compose.ui.test.filterToOne
14 | import androidx.compose.ui.test.hasClickAction
15 | import androidx.compose.ui.test.hasImeAction
16 | import androidx.compose.ui.test.junit4.createComposeRule
17 | import androidx.compose.ui.test.onChildren
18 | import androidx.compose.ui.test.onNodeWithTag
19 | import androidx.compose.ui.test.onNodeWithText
20 | import androidx.compose.ui.test.performClick
21 | import androidx.compose.ui.text.input.ImeAction
22 | import io.mockk.mockk
23 | import io.mockk.verify
24 | import org.junit.Rule
25 | import org.junit.Test
26 | import kotlin.test.assertEquals
27 |
28 | class MainActivityKtTest {
29 |
30 | @get:Rule
31 | val composeTestRule = createComposeRule()
32 |
33 | private interface OnAdbCommandClickedClass {
34 | fun onClick()
35 | }
36 |
37 | @Test
38 | fun permissionErrorMessage() {
39 | val onAdbCommandClicked = mockk(relaxed = true)
40 | composeTestRule.setContent {
41 | PermissionErrorMessage{ onAdbCommandClicked.onClick() }
42 | }
43 | composeTestRule.onNode(hasClickAction())
44 | .performClick()
45 | verify {
46 | onAdbCommandClicked.onClick()
47 | }
48 | }
49 |
50 | @Test
51 | fun proxyHost() {
52 | composeTestRule.setContent {
53 | Column {
54 | Row(modifier = Modifier.testTag("enabled")) {
55 | ProxyHost(enabled = true, value = "value1", onValueChanged = {})
56 | }
57 | Row(modifier = Modifier.testTag("disabled")) {
58 | ProxyHost(enabled = false, value = "value2", onValueChanged = {})
59 | }
60 | }
61 | }
62 | composeTestRule.onNodeWithTag("enabled")
63 | .onChildren()
64 | .filterToOne(hasImeAction(ImeAction.Default))
65 | .assertIsEnabled()
66 | .assertTextEquals("Proxy Host", includeEditableText = false)
67 | composeTestRule.onNodeWithTag("disabled")
68 | .onChildren()
69 | .filterToOne(hasImeAction(ImeAction.Default))
70 | .assertIsNotEnabled()
71 | .assertTextEquals("Proxy Host", includeEditableText = false)
72 | }
73 |
74 | @Test
75 | fun proxyPort() {
76 | composeTestRule.setContent {
77 | Column {
78 | Row(modifier = Modifier.testTag("enabled")) {
79 | ProxyPort(enabled = true, value = "value1", onValueChanged = {})
80 | }
81 | Row(modifier = Modifier.testTag("disabled")) {
82 | ProxyPort(enabled = false, value = "value2", onValueChanged = {})
83 | }
84 | }
85 | }
86 | composeTestRule.onNodeWithTag("enabled")
87 | .onChildren()
88 | .filterToOne(hasImeAction(ImeAction.Default))
89 | .assertIsEnabled()
90 | .assertTextEquals("Proxy Port", includeEditableText = false)
91 | composeTestRule.onNodeWithTag("disabled")
92 | .onChildren()
93 | .filterToOne(hasImeAction(ImeAction.Default))
94 | .assertIsNotEnabled()
95 | .assertTextEquals("Proxy Port", includeEditableText = false)
96 | }
97 |
98 | @Test
99 | fun proxyToggleSwitch() {
100 | var i = 0
101 | composeTestRule.setContent {
102 | ProxyToggleSwitch(enabled = true, checked = true, onCheckedChange = { i++ })
103 | }
104 |
105 | val clickable = composeTestRule.onNode(hasClickAction())
106 | clickable
107 | .assertIsEnabled()
108 | .assertIsOn()
109 | .assertIsDisplayed()
110 |
111 | clickable.performClick()
112 | assertEquals(1, i)
113 | }
114 |
115 | @Test
116 | fun adbSwitch() {
117 | var i = 0
118 | composeTestRule.setContent {
119 | AdbSwitch(enabled = true, checked = true, onCheckedChange = { i++ })
120 | }
121 |
122 | val clickable = composeTestRule.onNode(hasClickAction())
123 | clickable
124 | .assertIsEnabled()
125 | .assertIsOn()
126 | .assertIsDisplayed()
127 |
128 | clickable.performClick()
129 | assertEquals(1, i)
130 | }
131 |
132 | @Test
133 | fun adbSwitch_disabled() {
134 | composeTestRule.setContent {
135 | AdbSwitch(enabled = false, checked = false, onCheckedChange = { })
136 | }
137 | composeTestRule.onNode(hasClickAction())
138 | .assertIsNotEnabled()
139 | .assertIsOff()
140 | .assertIsDisplayed()
141 | composeTestRule.onNodeWithText("Adb")
142 | .assertIsDisplayed()
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.main
2 |
3 | import android.Manifest
4 | import android.app.Application
5 | import android.content.Intent
6 | import android.net.Uri
7 | import androidx.core.app.ShareCompat
8 | import androidx.lifecycle.AndroidViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
11 | import com.nagopy.android.debugassistant.BuildConfig
12 | import com.nagopy.android.debugassistant.domain.model.ProxyInfo
13 | import com.nagopy.android.debugassistant.domain.usecase.DisableAdbUseCase
14 | import com.nagopy.android.debugassistant.domain.usecase.DisableProxyUseCase
15 | import com.nagopy.android.debugassistant.domain.usecase.EnableAdbUseCase
16 | import com.nagopy.android.debugassistant.domain.usecase.EnableProxyUseCase
17 | import com.nagopy.android.debugassistant.domain.usecase.GetAdbStatusUseCase
18 | import com.nagopy.android.debugassistant.domain.usecase.GetPermissionStatusUseCase
19 | import com.nagopy.android.debugassistant.domain.usecase.GetProxyStatusUseCase
20 | import com.nagopy.android.debugassistant.domain.usecase.GetUserProxyInfoUseCase
21 | import com.nagopy.android.debugassistant.domain.usecase.PutUserProxyInfoUseCase
22 | import kotlinx.coroutines.flow.MutableStateFlow
23 | import kotlinx.coroutines.flow.SharingStarted
24 | import kotlinx.coroutines.flow.stateIn
25 | import kotlinx.coroutines.flow.update
26 |
27 | class MainViewModel(
28 | application: Application,
29 | private val getProxyStatusUseCase: GetProxyStatusUseCase,
30 | private val enableProxyUseCase: EnableProxyUseCase,
31 | private val disableProxyUseCase: DisableProxyUseCase,
32 | private val getAdbStatusUseCase: GetAdbStatusUseCase,
33 | private val enableAdbUseCase: EnableAdbUseCase,
34 | private val disableAdbUseCase: DisableAdbUseCase,
35 | private val getPermissionStatusUseCase: GetPermissionStatusUseCase,
36 | getUserProxyInfoUseCase: GetUserProxyInfoUseCase,
37 | private val putUserProxyInfoUseCase: PutUserProxyInfoUseCase,
38 | ) : AndroidViewModel(application) {
39 |
40 | private val _viewModelState = MutableStateFlow(MainUiState(isLoading = true))
41 | val viewModelState = _viewModelState.stateIn(viewModelScope, SharingStarted.Eagerly, _viewModelState.value)
42 |
43 | init {
44 | val proxyInfo = getUserProxyInfoUseCase.getUserProxyInfo()
45 | val isPermissionGranted = getPermissionStatusUseCase.isPermissionGranted(Manifest.permission.WRITE_SECURE_SETTINGS)
46 | val isProxyEnabled = getProxyStatusUseCase.isProxyEnabled()
47 | val isAdbEnabled = getAdbStatusUseCase.isAdbEnabled()
48 | _viewModelState.update {
49 | it.copy(
50 | isLoading = false,
51 | proxyHost = proxyInfo.host,
52 | proxyPort = proxyInfo.port,
53 | isPermissionGranted = isPermissionGranted,
54 | isProxyEnabled = isProxyEnabled,
55 | isAdbEnabled = isAdbEnabled,
56 | )
57 | }
58 | }
59 |
60 | fun updateStatus() {
61 | updatePermissionStatus()
62 | updateProxyStatus()
63 | updateAdbStatus()
64 | }
65 |
66 | private fun updateProxyStatus() {
67 | _viewModelState.update {
68 | it.copy(
69 | isProxyEnabled = getProxyStatusUseCase.isProxyEnabled()
70 | )
71 | }
72 | }
73 |
74 | private fun updateAdbStatus() {
75 | _viewModelState.update {
76 | it.copy(
77 | isAdbEnabled = getAdbStatusUseCase.isAdbEnabled()
78 | )
79 | }
80 | }
81 |
82 | private fun updatePermissionStatus() {
83 | _viewModelState.update {
84 | it.copy(
85 | isPermissionGranted = getPermissionStatusUseCase.isPermissionGranted(Manifest.permission.WRITE_SECURE_SETTINGS)
86 | )
87 | }
88 | }
89 |
90 | fun onAdbCommandClicked() {
91 | getApplication().let { application ->
92 | application.startActivity(
93 | ShareCompat.IntentBuilder(application)
94 | .setText("adb shell pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS")
95 | .setType("text/plain")
96 | .intent
97 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
98 | )
99 | }
100 | }
101 |
102 | fun onProxyHostChanged(newValue: String) {
103 | putUserProxyInfoUseCase.putUserProxyInfo(
104 | ProxyInfo(
105 | newValue,
106 | _viewModelState.value.proxyPort
107 | )
108 | )
109 | _viewModelState.update {
110 | it.copy(
111 | proxyHost = newValue
112 | )
113 | }
114 | }
115 |
116 | fun onProxyPortChanged(newValue: String) {
117 | putUserProxyInfoUseCase.putUserProxyInfo(
118 | ProxyInfo(
119 | _viewModelState.value.proxyHost,
120 | newValue
121 | )
122 | )
123 | _viewModelState.update {
124 | it.copy(
125 | proxyPort = newValue
126 | )
127 | }
128 | }
129 |
130 | fun onProxySwitchClicked(checked: Boolean) {
131 | if (checked) {
132 | enableProxyUseCase.enableProxy(viewModelState.value.proxyHost, viewModelState.value.proxyPort)
133 | } else {
134 | disableProxyUseCase.disableProxy()
135 | }
136 |
137 | updateProxyStatus()
138 | }
139 |
140 | fun onAdbSwitchClicked(checked: Boolean) {
141 | if (checked) {
142 | enableAdbUseCase.enableAdb()
143 | } else {
144 | disableAdbUseCase.disableAdb()
145 | }
146 |
147 | updateAdbStatus()
148 | }
149 |
150 | fun onHowToUseButtonClicked() {
151 | getApplication().let { application ->
152 | application.startActivity(
153 | Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/75py/DebugAssistant#install"))
154 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
155 | )
156 | }
157 | }
158 |
159 | fun onLicensesButtonClicked() {
160 | getApplication().let { application ->
161 | application.startActivity(
162 | Intent(application, OssLicensesMenuActivity::class.java)
163 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
164 | )
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # GitHub Copilot Instructions for Debug Assistant
2 |
3 | ## Project Overview
4 |
5 | Debug Assistant is an Android application designed to help Android developers by providing quick access to development tools through system tiles. The app enables toggling proxy settings and ADB (Android Debug Bridge) directly from the Quick Settings panel.
6 |
7 | **Package Name:** `com.nagopy.android.debugassistant`
8 | **Application ID:** `com.nagopy.android.debugassistant`
9 | **Language:** Kotlin
10 | **Min SDK:** 28 (Android 9.0)
11 | **Target SDK:** 34 (Android 14)
12 | **Architecture:** Clean Architecture with MVVM
13 |
14 | ## Project Structure
15 |
16 | This is a multi-module Android project with the following structure:
17 |
18 | ```
19 | debugassistant/
20 | ├── app/ # Main application module
21 | │ ├── src/main/
22 | │ │ ├── java/com/nagopy/android/debugassistant/
23 | │ │ │ ├── ui/ # UI components (Activities, ViewModels, Compose)
24 | │ │ │ │ ├── main/ # Main screen components
25 | │ │ │ │ ├── theme/ # Theme and styling
26 | │ │ │ │ └── tile/ # Quick Settings tile services
27 | │ │ │ ├── AppModule.kt # Koin dependency injection
28 | │ │ │ └── DebugAssistantApplication.kt
29 | │ │ └── AndroidManifest.xml
30 | │ └── src/androidTest/ # Instrumentation tests
31 | ├── data/
32 | │ └── repository/ # Data layer - Repository implementations
33 | │ ├── src/main/java/com/nagopy/android/debugassistant/data/repository/
34 | │ └── src/test/
35 | ├── domain/
36 | │ ├── model/ # Domain models and entities
37 | │ └── usecase/ # Business logic and use cases
38 | │ ├── src/main/java/com/nagopy/android/debugassistant/domain/usecase/
39 | │ └── src/test/
40 | └── build.gradle # Root build configuration
41 | ```
42 |
43 | ## Technology Stack & Dependencies
44 |
45 | ### Core Technologies
46 | - **Kotlin**: Primary programming language
47 | - **Android SDK**: Target SDK 34, Min SDK 28
48 | - **Jetpack Compose**: Modern UI toolkit (`compose_version = '1.5.0'`)
49 | - **Gradle**: Build system with Kotlin DSL support
50 |
51 | ### Architecture & Patterns
52 | - **Clean Architecture**: Separation of concerns with data/domain/ui layers
53 | - **MVVM**: Model-View-ViewModel pattern with Compose
54 | - **Repository Pattern**: Data access abstraction
55 | - **Use Case/Interactor Pattern**: Business logic encapsulation
56 | - **Dependency Injection**: Koin framework
57 |
58 | ### Testing
59 | - **JUnit**: Unit testing framework
60 | - **MockK**: Mocking framework for Kotlin
61 | - **Compose Testing**: UI testing with `androidx.compose.ui.test`
62 | - **Android Test**: Instrumentation tests
63 |
64 | ### Code Quality & Tooling
65 | - **Detekt**: Static code analysis (config: `detekt.yml`)
66 | - **Ktlint**: Code formatting and style enforcement
67 | - **GitHub Actions**: CI/CD pipeline
68 |
69 | ## Key Features & Components
70 |
71 | ### Main Features
72 | 1. **Proxy Toggle**: Enable/disable system HTTP proxy via Quick Settings
73 | 2. **ADB Toggle**: Enable/disable Android Debug Bridge via Quick Settings
74 | 3. **Permission Management**: Handles `WRITE_SECURE_SETTINGS` permission
75 |
76 | ### Core Components
77 | - **MainActivity**: Main screen with Compose UI
78 | - **ProxyTileService**: Quick Settings tile for proxy control
79 | - **AdbTileService**: Quick Settings tile for ADB control
80 | - **Repositories**: Data access for system settings
81 | - **Use Cases**: Business logic for enable/disable operations
82 |
83 | ## Coding Patterns & Conventions
84 |
85 | ### Package Structure
86 | - Base package: `com.nagopy.android.debugassistant`
87 | - Modules follow feature-based packaging
88 | - Clear separation between UI, domain, and data layers
89 |
90 | ### Naming Conventions
91 | - **Classes**: PascalCase (e.g., `MainActivity`, `EnableProxyUseCase`)
92 | - **Functions**: camelCase (e.g., `enableProxy()`, `onProxySwitchClicked()`)
93 | - **Composables**: PascalCase with `@Composable` annotation
94 | - **Variables**: camelCase (e.g., `isProxyEnabled`, `proxyHost`)
95 | - **Constants**: UPPER_SNAKE_CASE
96 |
97 | ### Compose UI Patterns
98 | - Use `@Composable` functions for UI components
99 | - State management with `remember` and state hoisting
100 | - Material Design components
101 | - Modifier chaining for styling
102 |
103 | ### Architecture Patterns
104 | ```kotlin
105 | // Use Case Interface
106 | interface EnableProxyUseCase {
107 | fun enableProxy(host: String, port: Int): Boolean
108 | }
109 |
110 | // Interactor Implementation
111 | class EnableProxyInteractor(
112 | private val repository: GlobalSettingsRepository
113 | ) : EnableProxyUseCase {
114 | override fun enableProxy(host: String, port: Int): Boolean {
115 | // Business logic implementation
116 | }
117 | }
118 |
119 | // Repository Interface
120 | interface GlobalSettingsRepository {
121 | fun putString(key: String, value: String): Boolean
122 | fun putInt(key: String, value: Int): Boolean
123 | }
124 | ```
125 |
126 | ### Dependency Injection with Koin
127 | ```kotlin
128 | val domainModule = module {
129 | single { EnableProxyInteractor(get()) }
130 | single { GlobalSettingRepositoryImpl(get()) }
131 | }
132 | ```
133 |
134 | ## Testing Approach
135 |
136 | ### Unit Tests
137 | - Test use cases/interactors with MockK
138 | - Focus on business logic validation
139 | - Repository pattern allows easy mocking
140 |
141 | ### UI Tests
142 | - Compose testing with `ComposeTestRule`
143 | - Test user interactions and state changes
144 | - UI component isolation testing
145 |
146 | ### Test Structure Example
147 | ```kotlin
148 | class EnableProxyInteractorTest {
149 | private lateinit var interactor: EnableProxyInteractor
150 | private lateinit var repository: GlobalSettingsRepository
151 |
152 | @Before
153 | fun setUp() {
154 | repository = mockk(relaxed = true)
155 | interactor = EnableProxyInteractor(repository)
156 | }
157 |
158 | @Test
159 | fun enableProxy_success() {
160 | every { repository.putString(any(), any()) } returns true
161 | val result = interactor.enableProxy("127.0.0.1", 8080)
162 | assertTrue(result)
163 | }
164 | }
165 | ```
166 |
167 | ## Build & Development
168 |
169 | ### Gradle Tasks
170 | - `./gradlew build` - Full build
171 | - `./gradlew testDebugUnitTest` - Run unit tests
172 | - `./gradlew detekt` - Run static analysis
173 | - `./gradlew ktlintCheck` - Check code formatting
174 |
175 | ### Code Quality Configuration
176 | - **Detekt**: Custom rules in `detekt.yml`
177 | - Disabled long parameter lists for Compose
178 | - Ignores private functions in complexity checks
179 | - Max line length: 140 characters
180 | - **Ktlint**: Standard Kotlin formatting rules
181 |
182 | ## Development Guidelines
183 |
184 | ### When Adding New Features
185 | 1. **Domain First**: Start with use case interfaces in domain module
186 | 2. **Repository Pattern**: Add repository interface and implementation
187 | 3. **Dependency Injection**: Register in appropriate Koin modules
188 | 4. **UI Layer**: Create Compose UI components with proper state management
189 | 5. **Testing**: Add unit tests for business logic and UI tests for interactions
190 |
191 | ### System Integration
192 | - Use `android.provider.Settings.Global` for system settings
193 | - Handle `WRITE_SECURE_SETTINGS` permission requirements
194 | - Implement proper error handling for system-level operations
195 |
196 | ### Tile Services
197 | - Extend `TileService` for Quick Settings integration
198 | - Handle tile state synchronization
199 | - Provide user feedback through tile states
200 |
201 | ## Common Patterns to Follow
202 |
203 | ### State Management in Compose
204 | ```kotlin
205 | @Composable
206 | fun ProxySection(
207 | isProxyEnabled: Boolean,
208 | proxyHost: String,
209 | onProxyHostChanged: (String) -> Unit,
210 | onProxySwitchClicked: (Boolean) -> Unit
211 | ) {
212 | // UI implementation with state hoisting
213 | }
214 | ```
215 |
216 | ### Use Case Implementation
217 | ```kotlin
218 | class EnableProxyInteractor(
219 | private val globalSettingsRepository: GlobalSettingsRepository
220 | ) : EnableProxyUseCase {
221 | override fun enableProxy(proxyInfo: ProxyInfo): Boolean {
222 | return try {
223 | globalSettingsRepository.putString(
224 | Settings.Global.HTTP_PROXY,
225 | "${proxyInfo.host}:${proxyInfo.port}"
226 | )
227 | } catch (e: Exception) {
228 | false
229 | }
230 | }
231 | }
232 | ```
233 |
234 | ### Repository Pattern
235 | ```kotlin
236 | interface GlobalSettingsRepository {
237 | fun getString(key: String): String?
238 | fun putString(key: String, value: String): Boolean
239 | fun getInt(key: String, defaultValue: Int = 0): Int
240 | fun putInt(key: String, value: Int): Boolean
241 | }
242 | ```
243 |
244 | ## CI/CD Integration
245 |
246 | The project uses GitHub Actions with:
247 | - **Java 17 (Temurin distribution)**
248 | - **Gradle caching** for faster builds
249 | - **Unit test execution** on push/PR
250 | - **Detekt analysis** for code quality
251 | - **Path ignoring** for documentation changes
252 |
253 | When suggesting code changes, ensure they:
254 | 1. Follow the established architecture patterns
255 | 2. Include appropriate error handling
256 | 3. Add corresponding tests
257 | 4. Meet code quality standards (Detekt/Ktlint)
258 | 5. Handle Android system permissions properly
259 |
260 | ## Security Considerations
261 |
262 | - **WRITE_SECURE_SETTINGS**: Required permission for system setting modifications
263 | - **System Settings**: Handle SecurityException when accessing restricted settings
264 | - **User Permissions**: Provide clear permission requirement information
265 | - **Error States**: Graceful degradation when permissions are not granted
266 |
267 | This application modifies sensitive system settings and should handle security restrictions appropriately.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nagopy/android/debugassistant/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.nagopy.android.debugassistant.ui.main
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.selection.toggleable
15 | import androidx.compose.foundation.text.KeyboardOptions
16 | import androidx.compose.foundation.verticalScroll
17 | import androidx.compose.material.Button
18 | import androidx.compose.material.ContentAlpha
19 | import androidx.compose.material.Divider
20 | import androidx.compose.material.LocalContentAlpha
21 | import androidx.compose.material.OutlinedTextField
22 | import androidx.compose.material.Scaffold
23 | import androidx.compose.material.Switch
24 | import androidx.compose.material.Text
25 | import androidx.compose.material.TopAppBar
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.runtime.collectAsState
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.draw.alpha
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.res.stringResource
32 | import androidx.compose.ui.semantics.Role
33 | import androidx.compose.ui.text.input.KeyboardType
34 | import androidx.compose.ui.tooling.preview.Preview
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import com.nagopy.android.debugassistant.BuildConfig
38 | import com.nagopy.android.debugassistant.R
39 | import com.nagopy.android.debugassistant.ui.theme.DebugAssistantTheme
40 | import org.koin.androidx.viewmodel.ext.android.viewModel
41 |
42 | class MainActivity : ComponentActivity() {
43 |
44 | private val mainViewModel: MainViewModel by viewModel()
45 |
46 | override fun onCreate(savedInstanceState: Bundle?) {
47 | super.onCreate(savedInstanceState)
48 | setContent {
49 | DebugAssistantTheme {
50 | Scaffold(
51 | topBar = {
52 | TopAppBar(
53 | title = { Text(stringResource(id = R.string.app_name)) },
54 | )
55 | },
56 | content = { padding ->
57 | val state = mainViewModel.viewModelState.collectAsState().value
58 | MainScreen(
59 | modifier = Modifier.padding(padding),
60 | proxyHost = state.proxyHost,
61 | proxyPort = state.proxyPort,
62 | isPermissionGranted = state.isPermissionGranted,
63 | isProxyEnabled = state.isProxyEnabled,
64 | isAdbEnabled = state.isAdbEnabled,
65 | onAdbCommandClicked = { mainViewModel.onAdbCommandClicked() },
66 | onProxyHostChanged = { mainViewModel.onProxyHostChanged(it) },
67 | onProxyPortChanged = { mainViewModel.onProxyPortChanged(it) },
68 | onProxySwitchClicked = { mainViewModel.onProxySwitchClicked(it) },
69 | onAdbSwitchClicked = { mainViewModel.onAdbSwitchClicked(it) },
70 | onHowToUseButtonClicked = { mainViewModel.onHowToUseButtonClicked() },
71 | onLicensesButtonClicked = { mainViewModel.onLicensesButtonClicked() },
72 | )
73 | }
74 | )
75 | }
76 | }
77 | }
78 |
79 | override fun onResume() {
80 | super.onResume()
81 | mainViewModel.updateStatus()
82 | }
83 | }
84 |
85 | @Preview(showBackground = true, showSystemUi = true)
86 | @Composable
87 | fun DefaultPreview() {
88 | DebugAssistantTheme {
89 | Scaffold(
90 | topBar = {
91 | TopAppBar(
92 | title = { Text(stringResource(id = R.string.app_name)) },
93 | )
94 | },
95 | content = { padding ->
96 | MainScreen(
97 | modifier = Modifier.padding(padding),
98 | proxyHost = "host",
99 | proxyPort = "port",
100 | isPermissionGranted = false,
101 | isProxyEnabled = true,
102 | isAdbEnabled = true,
103 | onAdbCommandClicked = {},
104 | onProxyHostChanged = {},
105 | onProxyPortChanged = {},
106 | onProxySwitchClicked = {},
107 | onAdbSwitchClicked = {},
108 | onHowToUseButtonClicked = {},
109 | onLicensesButtonClicked = {},
110 | )
111 | }
112 | )
113 | }
114 | }
115 |
116 | @Composable
117 | fun MainScreen(
118 | modifier: Modifier = Modifier,
119 | proxyHost: String,
120 | proxyPort: String,
121 | isPermissionGranted: Boolean,
122 | isProxyEnabled: Boolean,
123 | isAdbEnabled: Boolean,
124 | onAdbCommandClicked: () -> Unit,
125 | onProxyHostChanged: (String) -> Unit,
126 | onProxyPortChanged: (String) -> Unit,
127 | onProxySwitchClicked: (Boolean) -> Unit,
128 | onAdbSwitchClicked: (Boolean) -> Unit,
129 | onHowToUseButtonClicked: () -> Unit,
130 | onLicensesButtonClicked: () -> Unit,
131 | ) {
132 | Column(
133 | modifier
134 | .padding(16.dp)
135 | .verticalScroll(rememberScrollState())
136 | ) {
137 | if (!isPermissionGranted) {
138 | PermissionErrorMessage(onAdbCommandClicked = onAdbCommandClicked)
139 |
140 | Spacer(modifier = Modifier.height(16.dp))
141 | }
142 |
143 | ProxySection(
144 | isPermissionGranted = isPermissionGranted,
145 | proxyHost = proxyHost,
146 | onProxyHostChanged = onProxyHostChanged,
147 | proxyPort = proxyPort,
148 | onProxyPortChanged = onProxyPortChanged,
149 | isProxyEnabled = isProxyEnabled,
150 | onProxySwitchClicked = onProxySwitchClicked,
151 | )
152 |
153 | Spacer(modifier = Modifier.height(16.dp))
154 |
155 | AdbSection(
156 | isPermissionGranted = isPermissionGranted,
157 | isAdbEnabled = isAdbEnabled,
158 | onAdbSwitchClicked = onAdbSwitchClicked
159 | )
160 |
161 | Spacer(modifier = Modifier.height(16.dp))
162 |
163 | AboutSection(
164 | onHowToUseButtonClicked = onHowToUseButtonClicked,
165 | onLicensesButtonClicked = onLicensesButtonClicked,
166 | )
167 | }
168 | }
169 |
170 | @Composable
171 | fun PermissionErrorMessage(onAdbCommandClicked: () -> Unit) {
172 | Column {
173 | Text(
174 | text = stringResource(id = R.string.permission_error),
175 | color = Color.Red
176 | )
177 | Spacer(modifier = Modifier.height(8.dp))
178 | Button(onClick = { onAdbCommandClicked() }) {
179 | Text(
180 | text = "adb shell pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS",
181 | )
182 | }
183 | }
184 | }
185 |
186 | @Composable
187 | fun ProxySection(
188 | isPermissionGranted: Boolean,
189 | proxyHost: String,
190 | onProxyHostChanged: (String) -> Unit,
191 | proxyPort: String,
192 | onProxyPortChanged: (String) -> Unit,
193 | isProxyEnabled: Boolean,
194 | onProxySwitchClicked: (Boolean) -> Unit,
195 | ) {
196 | Column {
197 | Text(
198 | "Settings.Global.HTTP_PROXY", fontSize = 18.sp
199 | )
200 | Spacer(modifier = Modifier.height(3.dp))
201 | Divider()
202 |
203 | Spacer(modifier = Modifier.height(8.dp))
204 |
205 | Text(
206 | text = stringResource(id = R.string.caution_http_proxy),
207 | color = Color.Red,
208 | )
209 |
210 | Spacer(modifier = Modifier.height(8.dp))
211 |
212 | ProxyHost(
213 | enabled = isPermissionGranted,
214 | value = proxyHost,
215 | onValueChanged = {
216 | onProxyHostChanged(it)
217 | }
218 | )
219 |
220 | Spacer(modifier = Modifier.height(8.dp))
221 |
222 | ProxyPort(
223 | enabled = isPermissionGranted,
224 | value = proxyPort,
225 | onValueChanged = {
226 | onProxyPortChanged(it)
227 | }
228 | )
229 |
230 | Spacer(modifier = Modifier.height(4.dp))
231 |
232 | ProxyToggleSwitch(
233 | enabled = isPermissionGranted && proxyHost.isNotEmpty() && proxyPort.isNotEmpty(),
234 | checked = isProxyEnabled,
235 | ) {
236 | onProxySwitchClicked(it)
237 | }
238 | }
239 | }
240 |
241 | @Composable
242 | fun ProxyHost(enabled: Boolean, value: String, onValueChanged: (String) -> Unit) {
243 | OutlinedTextField(
244 | enabled = enabled,
245 | value = value,
246 | onValueChange = { onValueChanged(it) },
247 | label = { Text("Proxy Host") },
248 | maxLines = 1,
249 | keyboardOptions = KeyboardOptions(
250 | keyboardType = KeyboardType.Uri
251 | )
252 | )
253 | }
254 |
255 | @Composable
256 | fun ProxyPort(enabled: Boolean, value: String, onValueChanged: (String) -> Unit) {
257 | OutlinedTextField(
258 | enabled = enabled,
259 | value = value,
260 | onValueChange = { onValueChanged(it) },
261 | label = { Text("Proxy Port") },
262 | maxLines = 1,
263 | keyboardOptions = KeyboardOptions(
264 | keyboardType = KeyboardType.Number
265 | )
266 | )
267 | }
268 |
269 | @Composable
270 | fun ProxyToggleSwitch(enabled: Boolean, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
271 | val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
272 |
273 | Row(
274 | modifier = Modifier
275 | .toggleable(
276 | enabled = enabled,
277 | value = checked,
278 | role = Role.Switch,
279 | onValueChange = { onCheckedChange(it) }
280 | )
281 | .padding(16.dp)
282 | .fillMaxWidth()
283 | ) {
284 | Text(text = "Use Proxy", modifier = Modifier.alpha(alpha))
285 | Spacer(modifier = Modifier.width(8.dp))
286 | Switch(enabled = enabled, checked = checked, onCheckedChange = null)
287 | }
288 | }
289 |
290 | @Composable
291 | fun AdbSection(
292 | isPermissionGranted: Boolean,
293 | isAdbEnabled: Boolean,
294 | onAdbSwitchClicked: (Boolean) -> Unit,
295 | ) {
296 | Column {
297 | Text(
298 | "Settings.Global.ADB_ENABLED", fontSize = 18.sp
299 | )
300 | Spacer(modifier = Modifier.height(3.dp))
301 | Divider()
302 |
303 | Spacer(modifier = Modifier.height(8.dp))
304 |
305 | AdbSwitch(
306 | enabled = isPermissionGranted,
307 | checked = isAdbEnabled,
308 | onCheckedChange = onAdbSwitchClicked
309 | )
310 | }
311 | }
312 |
313 | @Composable
314 | fun AdbSwitch(enabled: Boolean, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
315 | val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
316 |
317 | Row(
318 | Modifier
319 | .toggleable(
320 | enabled = enabled,
321 | value = checked,
322 | role = Role.Switch,
323 | onValueChange = { onCheckedChange(it) }
324 | )
325 | .padding(16.dp)
326 | .fillMaxWidth()
327 | ) {
328 | Text(text = "Adb", modifier = Modifier.alpha(alpha))
329 | Spacer(modifier = Modifier.width(8.dp))
330 | Switch(enabled = enabled, checked = checked, onCheckedChange = null)
331 | }
332 | }
333 |
334 | @Composable
335 | fun AboutSection(
336 | onHowToUseButtonClicked: () -> Unit,
337 | onLicensesButtonClicked: () -> Unit,
338 | ) {
339 | Column {
340 | Text(
341 | "About", fontSize = 18.sp
342 | )
343 | Spacer(modifier = Modifier.height(3.dp))
344 | Divider()
345 |
346 | Spacer(modifier = Modifier.height(8.dp))
347 |
348 | Button(onClick = onHowToUseButtonClicked) {
349 | Text(stringResource(id = R.string.how_to_use))
350 | }
351 | Spacer(modifier = Modifier.height(4.dp))
352 | Button(onClick = onLicensesButtonClicked) {
353 | Text(stringResource(id = R.string.licenses))
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------