pakstr 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +12 -28
- package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +13 -1
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseSettings.kt +98 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseValidator.kt +30 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeConfig.kt +7 -12
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +70 -4
- package/android-template/app/src/main/res/layout/activity_developer_tools.xml +257 -66
- package/android-template/app/src/main/res/values/strings.xml +24 -5
- package/android-template/app/src/test/java/com/pakstr/app/PakstrBridgeRuntimeApiBaseTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseProxyTest.kt +49 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseSettingsTest.kt +107 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseValidatorTest.kt +50 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ build:
|
|
|
49
49
|
# apiBase: https://api.example.com # Optional packaged /api/* proxy target.
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`runtime.apiBase` must be an absolute HTTPS URL. It is public APK metadata, not a secret.
|
|
52
|
+
`runtime.apiBase` must be an absolute HTTPS URL. It is the immutable packaged/default proxy target and public APK metadata, not a secret. The effective target precedence is persistent runtime override, then packaged default, then disabled. A web app may call `window.PakstrBridge.setApiBaseUrl(url)` to validate and synchronously persist an HTTPS override, and `window.PakstrBridge.getApiBaseUrl()` to read the effective URL. Developer Tools uses the same setting. Reset removes the override and restores the packaged default; without a packaged default it disables `/api/*`. Changes affect the next proxy request without an app restart, LocalServer restart, or WebView reload.
|
|
53
53
|
|
|
54
54
|
### 3. Build, sign, and publish
|
|
55
55
|
```bash
|
|
@@ -80,6 +80,11 @@ internal fun localRequestUrl(
|
|
|
80
80
|
if (query.isNullOrEmpty()) "" else "?$query"
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
+
internal fun proxyApiUrl(
|
|
84
|
+
apiBase: String,
|
|
85
|
+
uri: String
|
|
86
|
+
): URL = URL(apiBase + uri)
|
|
87
|
+
|
|
83
88
|
internal data class ProxyHttpResponse(
|
|
84
89
|
val code: Int,
|
|
85
90
|
val contentType: String?,
|
|
@@ -258,7 +263,8 @@ class LocalServer(
|
|
|
258
263
|
)
|
|
259
264
|
if (uri.startsWith("/api/")) {
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
val apiBase = RuntimeConfig.apiBase(context)
|
|
267
|
+
if (apiBase == null) {
|
|
262
268
|
|
|
263
269
|
AppDebugLogger.network(
|
|
264
270
|
|
|
@@ -283,7 +289,7 @@ class LocalServer(
|
|
|
283
289
|
)
|
|
284
290
|
}
|
|
285
291
|
|
|
286
|
-
return proxyApi(session, uri)
|
|
292
|
+
return proxyApi(session, uri, apiBase)
|
|
287
293
|
|
|
288
294
|
}
|
|
289
295
|
|
|
@@ -370,7 +376,7 @@ class LocalServer(
|
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
private fun proxyApi(
|
|
373
|
-
session: IHTTPSession?, uri: String
|
|
379
|
+
session: IHTTPSession?, uri: String, apiBase: String
|
|
374
380
|
): Response {
|
|
375
381
|
|
|
376
382
|
if (AppDebugLogger.isNetworkLoggingEnabled(context)) {
|
|
@@ -386,29 +392,7 @@ class LocalServer(
|
|
|
386
392
|
|
|
387
393
|
return try {
|
|
388
394
|
|
|
389
|
-
val
|
|
390
|
-
|
|
391
|
-
val apiBase = config.optString("apiBase")
|
|
392
|
-
|
|
393
|
-
if (apiBase.isBlank()) {
|
|
394
|
-
AppDebugLogger.error(
|
|
395
|
-
|
|
396
|
-
context,
|
|
397
|
-
|
|
398
|
-
"API",
|
|
399
|
-
|
|
400
|
-
"apiBase missing"
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
return newFixedLengthResponse(
|
|
404
|
-
Response.Status.INTERNAL_ERROR,
|
|
405
|
-
"application/json",
|
|
406
|
-
"""{"error":"apiBase missing"}"""
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
val finalUrl = apiBase.removeSuffix("/") + uri
|
|
411
|
-
val url = URL(finalUrl)
|
|
395
|
+
val url = proxyApiUrl(apiBase, uri)
|
|
412
396
|
val method = session?.method?.name ?: "GET"
|
|
413
397
|
val requestHeaders = session?.headers ?: emptyMap()
|
|
414
398
|
val localRequestUrl = localRequestUrl(
|
|
@@ -438,7 +422,7 @@ class LocalServer(
|
|
|
438
422
|
if (AppDebugLogger.isNetworkLoggingEnabled(context)) {
|
|
439
423
|
AppDebugLogger.network(
|
|
440
424
|
context,
|
|
441
|
-
"
|
|
425
|
+
"Proxy response: ${response.code}"
|
|
442
426
|
)
|
|
443
427
|
}
|
|
444
428
|
return newFixedLengthResponse(
|
|
@@ -498,7 +482,7 @@ class LocalServer(
|
|
|
498
482
|
AppDebugLogger.network(
|
|
499
483
|
|
|
500
484
|
context,
|
|
501
|
-
"
|
|
485
|
+
"Proxy response: $code"
|
|
502
486
|
|
|
503
487
|
)
|
|
504
488
|
|
|
@@ -2,10 +2,22 @@ package com.pakstr.app
|
|
|
2
2
|
|
|
3
3
|
import android.webkit.JavascriptInterface
|
|
4
4
|
|
|
5
|
-
class PakstrBridge
|
|
5
|
+
class PakstrBridge(
|
|
6
|
+
private val runtimeApiBaseSettings: RuntimeApiBaseSettings
|
|
7
|
+
) {
|
|
6
8
|
|
|
7
9
|
@JavascriptInterface
|
|
8
10
|
fun getNip55CallbackUrl(): String {
|
|
9
11
|
return Nip55Callback.callbackPrefix
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
@JavascriptInterface
|
|
15
|
+
fun setApiBaseUrl(url: String): Boolean {
|
|
16
|
+
return runtimeApiBaseSettings.setOverride(url)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@JavascriptInterface
|
|
20
|
+
fun getApiBaseUrl(): String? {
|
|
21
|
+
return runtimeApiBaseSettings.current().url
|
|
22
|
+
}
|
|
11
23
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
|
|
6
|
+
class RuntimeApiBaseSettings private constructor(
|
|
7
|
+
private val preferences: Preferences,
|
|
8
|
+
private val packagedValueProvider: () -> String?
|
|
9
|
+
) {
|
|
10
|
+
enum class Source {
|
|
11
|
+
RUNTIME_OVERRIDE,
|
|
12
|
+
PACKAGED_DEFAULT,
|
|
13
|
+
DISABLED
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
data class Value(
|
|
17
|
+
val url: String?,
|
|
18
|
+
val source: Source
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
fun current(): Value = resolveEffectiveValue(
|
|
22
|
+
persistedOverride = preferences.getString(API_BASE_OVERRIDE_KEY),
|
|
23
|
+
packagedDefault = packagedValueProvider()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
fun setOverride(value: String): Boolean {
|
|
27
|
+
val canonical = RuntimeApiBaseValidator.validateAndCanonicalize(value)
|
|
28
|
+
?: return false
|
|
29
|
+
return preferences.putString(API_BASE_OVERRIDE_KEY, canonical)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun reset(): Boolean = preferences.remove(API_BASE_OVERRIDE_KEY)
|
|
33
|
+
|
|
34
|
+
internal interface Preferences {
|
|
35
|
+
fun getString(key: String): String?
|
|
36
|
+
fun putString(key: String, value: String): Boolean
|
|
37
|
+
fun remove(key: String): Boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private class SharedPreferencesAdapter(
|
|
41
|
+
private val preferences: SharedPreferences
|
|
42
|
+
) : Preferences {
|
|
43
|
+
override fun getString(key: String): String? = preferences.getString(key, null)
|
|
44
|
+
|
|
45
|
+
override fun putString(key: String, value: String): Boolean =
|
|
46
|
+
preferences.edit().putString(key, value).commit()
|
|
47
|
+
|
|
48
|
+
override fun remove(key: String): Boolean =
|
|
49
|
+
preferences.edit().remove(key).commit()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
companion object {
|
|
53
|
+
internal const val PREFERENCES_FILE = "pakstr_runtime_api_base"
|
|
54
|
+
internal const val API_BASE_OVERRIDE_KEY = "api_base_override"
|
|
55
|
+
|
|
56
|
+
fun from(context: Context): RuntimeApiBaseSettings {
|
|
57
|
+
val applicationContext = context.applicationContext
|
|
58
|
+
val preferences = applicationContext.getSharedPreferences(
|
|
59
|
+
PREFERENCES_FILE,
|
|
60
|
+
Context.MODE_PRIVATE
|
|
61
|
+
)
|
|
62
|
+
return RuntimeApiBaseSettings(
|
|
63
|
+
SharedPreferencesAdapter(preferences)
|
|
64
|
+
) {
|
|
65
|
+
RuntimeConfig.packagedApiBase(applicationContext)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
internal fun resolveEffectiveValue(
|
|
70
|
+
persistedOverride: String?,
|
|
71
|
+
packagedDefault: String?
|
|
72
|
+
): Value {
|
|
73
|
+
val override = RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
74
|
+
persistedOverride
|
|
75
|
+
)
|
|
76
|
+
if (override != null) {
|
|
77
|
+
return Value(override, Source.RUNTIME_OVERRIDE)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val packaged = RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
81
|
+
packagedDefault
|
|
82
|
+
)
|
|
83
|
+
return if (packaged != null) {
|
|
84
|
+
Value(packaged, Source.PACKAGED_DEFAULT)
|
|
85
|
+
} else {
|
|
86
|
+
Value(null, Source.DISABLED)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
internal fun createForTesting(
|
|
91
|
+
preferences: Preferences,
|
|
92
|
+
packagedValueProvider: () -> String?
|
|
93
|
+
): RuntimeApiBaseSettings = RuntimeApiBaseSettings(
|
|
94
|
+
preferences,
|
|
95
|
+
packagedValueProvider
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import java.net.URI
|
|
4
|
+
|
|
5
|
+
object RuntimeApiBaseValidator {
|
|
6
|
+
const val MAX_LENGTH = 2048
|
|
7
|
+
|
|
8
|
+
fun validateAndCanonicalize(value: String?): String? {
|
|
9
|
+
if (
|
|
10
|
+
value == null ||
|
|
11
|
+
value.isEmpty() ||
|
|
12
|
+
value.length > MAX_LENGTH ||
|
|
13
|
+
value != value.trim()
|
|
14
|
+
) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
val uri = runCatching { URI(value) }.getOrNull() ?: return null
|
|
19
|
+
if (!uri.isAbsolute || !uri.scheme.equals("https", ignoreCase = true)) return null
|
|
20
|
+
if (uri.host.isNullOrBlank()) return null
|
|
21
|
+
if (uri.rawUserInfo != null || uri.rawQuery != null || uri.rawFragment != null) return null
|
|
22
|
+
|
|
23
|
+
val host = uri.host.lowercase().let {
|
|
24
|
+
if (it.contains(':') && !it.startsWith("[")) "[$it]" else it
|
|
25
|
+
}
|
|
26
|
+
val port = if (uri.port == -1) "" else ":${uri.port}"
|
|
27
|
+
val path = uri.rawPath.orEmpty().trimEnd('/')
|
|
28
|
+
return "https://$host$port$path"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -58,19 +58,14 @@ object RuntimeConfig {
|
|
|
58
58
|
*/
|
|
59
59
|
fun apiBase(
|
|
60
60
|
context: Context
|
|
61
|
-
): String?
|
|
61
|
+
): String? = RuntimeApiBaseSettings.from(context).current().url
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return value.takeIf {
|
|
72
|
-
it.isNotBlank()
|
|
73
|
-
}
|
|
63
|
+
internal fun packagedApiBase(
|
|
64
|
+
context: Context
|
|
65
|
+
): String? = load(context).optString(
|
|
66
|
+
"apiBase", ""
|
|
67
|
+
).takeIf {
|
|
68
|
+
it.isNotBlank()
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
/**
|
|
@@ -15,13 +15,18 @@ import android.widget.TextView
|
|
|
15
15
|
import android.widget.Toast
|
|
16
16
|
import androidx.activity.enableEdgeToEdge
|
|
17
17
|
import androidx.appcompat.app.AppCompatActivity
|
|
18
|
+
import androidx.core.view.ViewCompat
|
|
19
|
+
import androidx.core.view.WindowInsetsCompat
|
|
20
|
+
import androidx.core.view.updatePadding
|
|
18
21
|
import com.google.android.material.appbar.MaterialToolbar
|
|
19
22
|
import com.google.android.material.button.MaterialButton
|
|
20
23
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
21
24
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
25
|
+
import com.google.android.material.textfield.TextInputEditText
|
|
26
|
+
import com.google.android.material.textfield.TextInputLayout
|
|
22
27
|
import com.pakstr.app.BuildConfig
|
|
23
28
|
import com.pakstr.app.R
|
|
24
|
-
import com.pakstr.app.
|
|
29
|
+
import com.pakstr.app.RuntimeApiBaseSettings
|
|
25
30
|
|
|
26
31
|
class DeveloperToolsActivity : AppCompatActivity() {
|
|
27
32
|
|
|
@@ -30,8 +35,12 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
30
35
|
private lateinit var uptimeValue: TextView
|
|
31
36
|
private lateinit var debugModeSwitch: SwitchMaterial
|
|
32
37
|
private lateinit var apiSwitch: SwitchMaterial
|
|
38
|
+
private lateinit var apiBaseInputLayout: TextInputLayout
|
|
39
|
+
private lateinit var apiBaseInput: TextInputEditText
|
|
40
|
+
private lateinit var apiBaseSource: TextView
|
|
33
41
|
private lateinit var debugWebViewStatus: TextView
|
|
34
42
|
private lateinit var webViewStatus: TextView
|
|
43
|
+
private lateinit var runtimeApiBaseSettings: RuntimeApiBaseSettings
|
|
35
44
|
|
|
36
45
|
private val uptimeUpdater = object : Runnable {
|
|
37
46
|
override fun run() {
|
|
@@ -50,11 +59,14 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
setContentView(R.layout.activity_developer_tools)
|
|
62
|
+
runtimeApiBaseSettings = RuntimeApiBaseSettings.from(this)
|
|
53
63
|
|
|
64
|
+
setupWindowInsets()
|
|
54
65
|
setupToolbar()
|
|
55
66
|
bindViews()
|
|
56
67
|
populateRuntimeDetails()
|
|
57
68
|
setupDebugControls()
|
|
69
|
+
setupApiBaseControls()
|
|
58
70
|
setupLogActions()
|
|
59
71
|
setupCrashTesting()
|
|
60
72
|
setupWebViewActions()
|
|
@@ -71,6 +83,18 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
71
83
|
super.onStop()
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
private fun setupWindowInsets() {
|
|
87
|
+
ViewCompat.setOnApplyWindowInsetsListener(
|
|
88
|
+
findViewById(R.id.developerToolsRoot)
|
|
89
|
+
) { view, insets ->
|
|
90
|
+
val bars = insets.getInsets(
|
|
91
|
+
WindowInsetsCompat.Type.systemBars()
|
|
92
|
+
)
|
|
93
|
+
view.updatePadding(top = bars.top)
|
|
94
|
+
insets
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
private fun setupToolbar() {
|
|
75
99
|
findViewById<MaterialToolbar>(R.id.developerToolsToolbar).setNavigationOnClickListener {
|
|
76
100
|
finish()
|
|
@@ -81,6 +105,9 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
81
105
|
uptimeValue = findViewById(R.id.runtimeUptimeValue)
|
|
82
106
|
debugModeSwitch = findViewById(R.id.debugModeSwitch)
|
|
83
107
|
apiSwitch = findViewById(R.id.apiEnabledSwitch)
|
|
108
|
+
apiBaseInputLayout = findViewById(R.id.apiBaseInputLayout)
|
|
109
|
+
apiBaseInput = findViewById(R.id.apiBaseInput)
|
|
110
|
+
apiBaseSource = findViewById(R.id.apiBaseSource)
|
|
84
111
|
debugWebViewStatus = findViewById(R.id.debugWebViewStatus)
|
|
85
112
|
webViewStatus = findViewById(R.id.webViewStatus)
|
|
86
113
|
}
|
|
@@ -101,8 +128,6 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
private fun setupDebugControls() {
|
|
104
|
-
apiSwitch.isChecked = RuntimeConfig.apiEnabled(this)
|
|
105
|
-
|
|
106
131
|
debugModeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
|
107
132
|
if (isChecked) {
|
|
108
133
|
DeveloperToolsManager.enableDebug(this)
|
|
@@ -115,9 +140,50 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
143
|
+
private fun setupApiBaseControls() {
|
|
144
|
+
findViewById<MaterialButton>(R.id.saveApiBaseButton).setOnClickListener {
|
|
145
|
+
val value = apiBaseInput.text?.toString().orEmpty()
|
|
146
|
+
if (runtimeApiBaseSettings.setOverride(value)) {
|
|
147
|
+
apiBaseInputLayout.error = null
|
|
148
|
+
refreshApiBase()
|
|
149
|
+
showMessage(R.string.developer_tools_api_base_saved)
|
|
150
|
+
} else {
|
|
151
|
+
apiBaseInputLayout.error = getString(
|
|
152
|
+
R.string.developer_tools_api_base_invalid
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
findViewById<MaterialButton>(R.id.resetApiBaseButton).setOnClickListener {
|
|
157
|
+
if (runtimeApiBaseSettings.reset()) {
|
|
158
|
+
apiBaseInputLayout.error = null
|
|
159
|
+
refreshApiBase()
|
|
160
|
+
showMessage(R.string.developer_tools_api_base_reset)
|
|
161
|
+
} else {
|
|
162
|
+
showMessage(R.string.developer_tools_api_base_save_failed)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
refreshApiBase()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun refreshApiBase() {
|
|
169
|
+
val current = runtimeApiBaseSettings.current()
|
|
170
|
+
apiSwitch.isChecked = current.url != null
|
|
171
|
+
apiBaseInput.setText(current.url.orEmpty())
|
|
172
|
+
apiBaseSource.setText(
|
|
173
|
+
when (current.source) {
|
|
174
|
+
RuntimeApiBaseSettings.Source.RUNTIME_OVERRIDE ->
|
|
175
|
+
R.string.developer_tools_api_base_runtime_override
|
|
176
|
+
RuntimeApiBaseSettings.Source.PACKAGED_DEFAULT ->
|
|
177
|
+
R.string.developer_tools_api_base_packaged_default
|
|
178
|
+
RuntimeApiBaseSettings.Source.DISABLED ->
|
|
179
|
+
R.string.developer_tools_api_base_not_configured
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
private fun refreshStatuses() {
|
|
119
185
|
debugModeSwitch.isChecked = DeveloperToolsManager.isEnabled(this)
|
|
120
|
-
|
|
186
|
+
refreshApiBase()
|
|
121
187
|
|
|
122
188
|
val status = if (isWebViewDebugEnabled()) {
|
|
123
189
|
R.string.developer_tools_enabled
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
3
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
android:id="@+id/developerToolsRoot"
|
|
4
5
|
android:layout_width="match_parent"
|
|
5
6
|
android:layout_height="match_parent"
|
|
6
|
-
android:paddingBottom="56dp"
|
|
7
7
|
android:background="?android:attr/colorBackground"
|
|
8
|
-
android:orientation="vertical"
|
|
8
|
+
android:orientation="vertical"
|
|
9
|
+
android:paddingBottom="56dp">
|
|
9
10
|
|
|
10
11
|
<com.google.android.material.appbar.MaterialToolbar
|
|
11
12
|
android:id="@+id/developerToolsToolbar"
|
|
12
13
|
android:layout_width="match_parent"
|
|
13
14
|
android:layout_height="?attr/actionBarSize"
|
|
14
15
|
android:background="?attr/colorSurface"
|
|
15
|
-
android:elevation="4dp"
|
|
16
16
|
app:navigationIcon="?attr/homeAsUpIndicator"
|
|
17
17
|
app:title="@string/developer_tools_title" />
|
|
18
18
|
|
|
@@ -20,98 +20,132 @@
|
|
|
20
20
|
android:layout_width="match_parent"
|
|
21
21
|
android:layout_height="0dp"
|
|
22
22
|
android:layout_weight="1"
|
|
23
|
+
android:clipToPadding="false"
|
|
23
24
|
android:fillViewport="true">
|
|
24
25
|
|
|
25
26
|
<LinearLayout
|
|
26
27
|
android:layout_width="match_parent"
|
|
27
28
|
android:layout_height="wrap_content"
|
|
28
29
|
android:orientation="vertical"
|
|
29
|
-
android:
|
|
30
|
+
android:paddingStart="16dp"
|
|
31
|
+
android:paddingTop="12dp"
|
|
32
|
+
android:paddingEnd="16dp"
|
|
33
|
+
android:paddingBottom="24dp">
|
|
34
|
+
|
|
35
|
+
<TextView
|
|
36
|
+
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
37
|
+
android:layout_width="match_parent"
|
|
38
|
+
android:layout_height="wrap_content"
|
|
39
|
+
android:layout_marginStart="4dp"
|
|
40
|
+
android:layout_marginEnd="4dp"
|
|
41
|
+
android:layout_marginBottom="20dp"
|
|
42
|
+
android:text="@string/developer_tools_description"
|
|
43
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
30
44
|
|
|
31
45
|
<com.google.android.material.card.MaterialCardView
|
|
32
46
|
android:layout_width="match_parent"
|
|
33
47
|
android:layout_height="wrap_content"
|
|
34
48
|
android:layout_marginBottom="16dp"
|
|
35
|
-
app:cardCornerRadius="
|
|
36
|
-
app:cardElevation="
|
|
49
|
+
app:cardCornerRadius="16dp"
|
|
50
|
+
app:cardElevation="0dp"
|
|
51
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
52
|
+
app:strokeWidth="1dp">
|
|
37
53
|
|
|
38
54
|
<LinearLayout
|
|
39
55
|
android:layout_width="match_parent"
|
|
40
56
|
android:layout_height="wrap_content"
|
|
41
57
|
android:orientation="vertical"
|
|
42
|
-
android:padding="
|
|
58
|
+
android:padding="20dp">
|
|
43
59
|
|
|
44
60
|
<TextView
|
|
45
61
|
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
46
62
|
android:layout_width="wrap_content"
|
|
47
63
|
android:layout_height="wrap_content"
|
|
48
|
-
android:layout_marginBottom="16dp"
|
|
49
64
|
android:text="@string/developer_tools_runtime" />
|
|
50
65
|
|
|
66
|
+
<TextView
|
|
67
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
68
|
+
android:layout_width="match_parent"
|
|
69
|
+
android:layout_height="wrap_content"
|
|
70
|
+
android:layout_marginTop="2dp"
|
|
71
|
+
android:layout_marginBottom="20dp"
|
|
72
|
+
android:text="@string/developer_tools_runtime_description"
|
|
73
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
74
|
+
|
|
51
75
|
<TextView
|
|
52
76
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
53
77
|
android:layout_width="wrap_content"
|
|
54
78
|
android:layout_height="wrap_content"
|
|
55
|
-
android:text="@string/developer_tools_session_id"
|
|
79
|
+
android:text="@string/developer_tools_session_id"
|
|
80
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
56
81
|
|
|
57
82
|
<TextView
|
|
58
83
|
android:id="@+id/runtimeSessionIdValue"
|
|
59
84
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
60
85
|
android:layout_width="match_parent"
|
|
61
86
|
android:layout_height="wrap_content"
|
|
62
|
-
android:
|
|
87
|
+
android:layout_marginTop="2dp"
|
|
88
|
+
android:layout_marginBottom="14dp"
|
|
63
89
|
android:textIsSelectable="true" />
|
|
64
90
|
|
|
65
91
|
<TextView
|
|
66
92
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
67
93
|
android:layout_width="wrap_content"
|
|
68
94
|
android:layout_height="wrap_content"
|
|
69
|
-
android:text="@string/developer_tools_app_version"
|
|
95
|
+
android:text="@string/developer_tools_app_version"
|
|
96
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
70
97
|
|
|
71
98
|
<TextView
|
|
72
99
|
android:id="@+id/runtimeAppVersionValue"
|
|
73
100
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
74
101
|
android:layout_width="match_parent"
|
|
75
102
|
android:layout_height="wrap_content"
|
|
76
|
-
android:
|
|
103
|
+
android:layout_marginTop="2dp"
|
|
104
|
+
android:layout_marginBottom="14dp" />
|
|
77
105
|
|
|
78
106
|
<TextView
|
|
79
107
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
80
108
|
android:layout_width="wrap_content"
|
|
81
109
|
android:layout_height="wrap_content"
|
|
82
|
-
android:text="@string/developer_tools_android_version"
|
|
110
|
+
android:text="@string/developer_tools_android_version"
|
|
111
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
83
112
|
|
|
84
113
|
<TextView
|
|
85
114
|
android:id="@+id/runtimeAndroidVersionValue"
|
|
86
115
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
87
116
|
android:layout_width="match_parent"
|
|
88
117
|
android:layout_height="wrap_content"
|
|
89
|
-
android:
|
|
118
|
+
android:layout_marginTop="2dp"
|
|
119
|
+
android:layout_marginBottom="14dp" />
|
|
90
120
|
|
|
91
121
|
<TextView
|
|
92
122
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
93
123
|
android:layout_width="wrap_content"
|
|
94
124
|
android:layout_height="wrap_content"
|
|
95
|
-
android:text="@string/developer_tools_device_model"
|
|
125
|
+
android:text="@string/developer_tools_device_model"
|
|
126
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
96
127
|
|
|
97
128
|
<TextView
|
|
98
129
|
android:id="@+id/runtimeDeviceModelValue"
|
|
99
130
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
100
131
|
android:layout_width="match_parent"
|
|
101
132
|
android:layout_height="wrap_content"
|
|
102
|
-
android:
|
|
133
|
+
android:layout_marginTop="2dp"
|
|
134
|
+
android:layout_marginBottom="14dp" />
|
|
103
135
|
|
|
104
136
|
<TextView
|
|
105
137
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
106
138
|
android:layout_width="wrap_content"
|
|
107
139
|
android:layout_height="wrap_content"
|
|
108
|
-
android:text="@string/developer_tools_app_uptime"
|
|
140
|
+
android:text="@string/developer_tools_app_uptime"
|
|
141
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
109
142
|
|
|
110
143
|
<TextView
|
|
111
144
|
android:id="@+id/runtimeUptimeValue"
|
|
112
145
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
113
146
|
android:layout_width="match_parent"
|
|
114
|
-
android:layout_height="wrap_content"
|
|
147
|
+
android:layout_height="wrap_content"
|
|
148
|
+
android:layout_marginTop="2dp" />
|
|
115
149
|
</LinearLayout>
|
|
116
150
|
</com.google.android.material.card.MaterialCardView>
|
|
117
151
|
|
|
@@ -119,33 +153,100 @@
|
|
|
119
153
|
android:layout_width="match_parent"
|
|
120
154
|
android:layout_height="wrap_content"
|
|
121
155
|
android:layout_marginBottom="16dp"
|
|
122
|
-
app:cardCornerRadius="
|
|
123
|
-
app:cardElevation="
|
|
156
|
+
app:cardCornerRadius="16dp"
|
|
157
|
+
app:cardElevation="0dp"
|
|
158
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
159
|
+
app:strokeWidth="1dp">
|
|
124
160
|
|
|
125
161
|
<LinearLayout
|
|
126
162
|
android:layout_width="match_parent"
|
|
127
163
|
android:layout_height="wrap_content"
|
|
128
164
|
android:orientation="vertical"
|
|
129
|
-
android:padding="
|
|
165
|
+
android:padding="20dp">
|
|
130
166
|
|
|
131
167
|
<TextView
|
|
132
168
|
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
133
169
|
android:layout_width="wrap_content"
|
|
134
170
|
android:layout_height="wrap_content"
|
|
135
|
-
android:layout_marginBottom="8dp"
|
|
136
171
|
android:text="@string/developer_tools_debug" />
|
|
137
172
|
|
|
173
|
+
<TextView
|
|
174
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
175
|
+
android:layout_width="match_parent"
|
|
176
|
+
android:layout_height="wrap_content"
|
|
177
|
+
android:layout_marginTop="2dp"
|
|
178
|
+
android:layout_marginBottom="12dp"
|
|
179
|
+
android:text="@string/developer_tools_debug_description"
|
|
180
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
181
|
+
|
|
138
182
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
|
139
183
|
android:id="@+id/debugModeSwitch"
|
|
140
184
|
android:layout_width="match_parent"
|
|
141
185
|
android:layout_height="wrap_content"
|
|
186
|
+
android:minHeight="48dp"
|
|
142
187
|
android:text="@string/developer_tools_enable_debug_mode" />
|
|
143
188
|
|
|
189
|
+
<LinearLayout
|
|
190
|
+
android:layout_width="match_parent"
|
|
191
|
+
android:layout_height="wrap_content"
|
|
192
|
+
android:layout_marginTop="8dp"
|
|
193
|
+
android:gravity="center_vertical"
|
|
194
|
+
android:minHeight="48dp"
|
|
195
|
+
android:orientation="horizontal">
|
|
196
|
+
|
|
197
|
+
<TextView
|
|
198
|
+
style="@style/TextAppearance.MaterialComponents.Body1"
|
|
199
|
+
android:layout_width="0dp"
|
|
200
|
+
android:layout_height="wrap_content"
|
|
201
|
+
android:layout_weight="1"
|
|
202
|
+
android:text="@string/developer_tools_webview_debug" />
|
|
203
|
+
|
|
204
|
+
<TextView
|
|
205
|
+
android:id="@+id/debugWebViewStatus"
|
|
206
|
+
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
207
|
+
android:layout_width="wrap_content"
|
|
208
|
+
android:layout_height="wrap_content"
|
|
209
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
210
|
+
</LinearLayout>
|
|
211
|
+
</LinearLayout>
|
|
212
|
+
</com.google.android.material.card.MaterialCardView>
|
|
213
|
+
|
|
214
|
+
<com.google.android.material.card.MaterialCardView
|
|
215
|
+
android:layout_width="match_parent"
|
|
216
|
+
android:layout_height="wrap_content"
|
|
217
|
+
android:layout_marginBottom="16dp"
|
|
218
|
+
app:cardCornerRadius="16dp"
|
|
219
|
+
app:cardElevation="0dp"
|
|
220
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
221
|
+
app:strokeWidth="1dp">
|
|
222
|
+
|
|
223
|
+
<LinearLayout
|
|
224
|
+
android:layout_width="match_parent"
|
|
225
|
+
android:layout_height="wrap_content"
|
|
226
|
+
android:orientation="vertical"
|
|
227
|
+
android:padding="20dp">
|
|
228
|
+
|
|
229
|
+
<TextView
|
|
230
|
+
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
231
|
+
android:layout_width="wrap_content"
|
|
232
|
+
android:layout_height="wrap_content"
|
|
233
|
+
android:text="@string/developer_tools_api_proxy" />
|
|
234
|
+
|
|
235
|
+
<TextView
|
|
236
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
237
|
+
android:layout_width="match_parent"
|
|
238
|
+
android:layout_height="wrap_content"
|
|
239
|
+
android:layout_marginTop="2dp"
|
|
240
|
+
android:layout_marginBottom="8dp"
|
|
241
|
+
android:text="@string/developer_tools_api_proxy_description"
|
|
242
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
243
|
+
|
|
144
244
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
|
145
245
|
android:id="@+id/apiEnabledSwitch"
|
|
146
246
|
android:layout_width="match_parent"
|
|
147
247
|
android:layout_height="wrap_content"
|
|
148
248
|
android:enabled="false"
|
|
249
|
+
android:minHeight="48dp"
|
|
149
250
|
android:text="@string/developer_tools_enable_api" />
|
|
150
251
|
|
|
151
252
|
<TextView
|
|
@@ -154,7 +255,58 @@
|
|
|
154
255
|
android:layout_height="wrap_content"
|
|
155
256
|
android:layout_marginStart="4dp"
|
|
156
257
|
android:layout_marginBottom="16dp"
|
|
157
|
-
android:text="@string/
|
|
258
|
+
android:text="@string/developer_tools_api_effective_status"
|
|
259
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
260
|
+
|
|
261
|
+
<TextView
|
|
262
|
+
style="@style/TextAppearance.MaterialComponents.Overline"
|
|
263
|
+
android:layout_width="match_parent"
|
|
264
|
+
android:layout_height="wrap_content"
|
|
265
|
+
android:layout_marginBottom="8dp"
|
|
266
|
+
android:text="@string/developer_tools_api_effective_configuration"
|
|
267
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
268
|
+
|
|
269
|
+
<com.google.android.material.card.MaterialCardView
|
|
270
|
+
android:layout_width="match_parent"
|
|
271
|
+
android:layout_height="wrap_content"
|
|
272
|
+
android:layout_marginBottom="16dp"
|
|
273
|
+
app:cardCornerRadius="12dp"
|
|
274
|
+
app:cardElevation="0dp"
|
|
275
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
276
|
+
app:strokeWidth="1dp">
|
|
277
|
+
|
|
278
|
+
<LinearLayout
|
|
279
|
+
android:layout_width="match_parent"
|
|
280
|
+
android:layout_height="wrap_content"
|
|
281
|
+
android:orientation="vertical"
|
|
282
|
+
android:padding="16dp">
|
|
283
|
+
|
|
284
|
+
<com.google.android.material.textfield.TextInputLayout
|
|
285
|
+
android:id="@+id/apiBaseInputLayout"
|
|
286
|
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
287
|
+
android:layout_width="match_parent"
|
|
288
|
+
android:layout_height="wrap_content"
|
|
289
|
+
android:hint="@string/developer_tools_api_base_url">
|
|
290
|
+
|
|
291
|
+
<com.google.android.material.textfield.TextInputEditText
|
|
292
|
+
android:id="@+id/apiBaseInput"
|
|
293
|
+
android:layout_width="match_parent"
|
|
294
|
+
android:layout_height="wrap_content"
|
|
295
|
+
android:inputType="textUri"
|
|
296
|
+
android:maxLength="2048"
|
|
297
|
+
android:singleLine="true" />
|
|
298
|
+
</com.google.android.material.textfield.TextInputLayout>
|
|
299
|
+
|
|
300
|
+
<TextView
|
|
301
|
+
android:id="@+id/apiBaseSource"
|
|
302
|
+
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
303
|
+
android:layout_width="match_parent"
|
|
304
|
+
android:layout_height="wrap_content"
|
|
305
|
+
android:layout_marginStart="4dp"
|
|
306
|
+
android:layout_marginTop="8dp"
|
|
307
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
308
|
+
</LinearLayout>
|
|
309
|
+
</com.google.android.material.card.MaterialCardView>
|
|
158
310
|
|
|
159
311
|
<LinearLayout
|
|
160
312
|
android:layout_width="match_parent"
|
|
@@ -162,18 +314,21 @@
|
|
|
162
314
|
android:gravity="center_vertical"
|
|
163
315
|
android:orientation="horizontal">
|
|
164
316
|
|
|
165
|
-
<
|
|
166
|
-
|
|
317
|
+
<com.google.android.material.button.MaterialButton
|
|
318
|
+
android:id="@+id/saveApiBaseButton"
|
|
167
319
|
android:layout_width="0dp"
|
|
168
|
-
android:layout_height="
|
|
320
|
+
android:layout_height="match_parent"
|
|
169
321
|
android:layout_weight="1"
|
|
170
|
-
android:text="@string/
|
|
322
|
+
android:text="@string/developer_tools_api_base_save" />
|
|
171
323
|
|
|
172
|
-
<
|
|
173
|
-
android:id="@+id/
|
|
174
|
-
style="@style/
|
|
175
|
-
android:layout_width="
|
|
176
|
-
android:layout_height="
|
|
324
|
+
<com.google.android.material.button.MaterialButton
|
|
325
|
+
android:id="@+id/resetApiBaseButton"
|
|
326
|
+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
327
|
+
android:layout_width="0dp"
|
|
328
|
+
android:layout_height="match_parent"
|
|
329
|
+
android:layout_marginStart="8dp"
|
|
330
|
+
android:layout_weight="1"
|
|
331
|
+
android:text="@string/developer_tools_api_base_reset_button" />
|
|
177
332
|
</LinearLayout>
|
|
178
333
|
</LinearLayout>
|
|
179
334
|
</com.google.android.material.card.MaterialCardView>
|
|
@@ -182,22 +337,32 @@
|
|
|
182
337
|
android:layout_width="match_parent"
|
|
183
338
|
android:layout_height="wrap_content"
|
|
184
339
|
android:layout_marginBottom="16dp"
|
|
185
|
-
app:cardCornerRadius="
|
|
186
|
-
app:cardElevation="
|
|
340
|
+
app:cardCornerRadius="16dp"
|
|
341
|
+
app:cardElevation="0dp"
|
|
342
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
343
|
+
app:strokeWidth="1dp">
|
|
187
344
|
|
|
188
345
|
<LinearLayout
|
|
189
346
|
android:layout_width="match_parent"
|
|
190
347
|
android:layout_height="wrap_content"
|
|
191
348
|
android:orientation="vertical"
|
|
192
|
-
android:padding="
|
|
349
|
+
android:padding="20dp">
|
|
193
350
|
|
|
194
351
|
<TextView
|
|
195
352
|
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
196
353
|
android:layout_width="wrap_content"
|
|
197
354
|
android:layout_height="wrap_content"
|
|
198
|
-
android:layout_marginBottom="8dp"
|
|
199
355
|
android:text="@string/developer_tools_logs" />
|
|
200
356
|
|
|
357
|
+
<TextView
|
|
358
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
359
|
+
android:layout_width="match_parent"
|
|
360
|
+
android:layout_height="wrap_content"
|
|
361
|
+
android:layout_marginTop="2dp"
|
|
362
|
+
android:layout_marginBottom="16dp"
|
|
363
|
+
android:text="@string/developer_tools_logs_description"
|
|
364
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
365
|
+
|
|
201
366
|
<com.google.android.material.button.MaterialButton
|
|
202
367
|
android:id="@+id/viewLogsButton"
|
|
203
368
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
@@ -210,18 +375,22 @@
|
|
|
210
375
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
211
376
|
android:layout_width="match_parent"
|
|
212
377
|
android:layout_height="wrap_content"
|
|
378
|
+
android:layout_marginTop="4dp"
|
|
213
379
|
android:text="@string/developer_tools_clear_logs" />
|
|
214
380
|
|
|
215
381
|
<com.google.android.material.button.MaterialButton
|
|
216
382
|
android:id="@+id/exportDebugReportButton"
|
|
217
383
|
android:layout_width="match_parent"
|
|
218
384
|
android:layout_height="wrap_content"
|
|
385
|
+
android:layout_marginTop="12dp"
|
|
219
386
|
android:text="@string/developer_tools_export_debug_report" />
|
|
220
387
|
|
|
221
388
|
<com.google.android.material.button.MaterialButton
|
|
222
389
|
android:id="@+id/shareReportButton"
|
|
390
|
+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
223
391
|
android:layout_width="match_parent"
|
|
224
392
|
android:layout_height="wrap_content"
|
|
393
|
+
android:layout_marginTop="4dp"
|
|
225
394
|
android:text="@string/developer_tools_share_report" />
|
|
226
395
|
</LinearLayout>
|
|
227
396
|
</com.google.android.material.card.MaterialCardView>
|
|
@@ -230,55 +399,38 @@
|
|
|
230
399
|
android:layout_width="match_parent"
|
|
231
400
|
android:layout_height="wrap_content"
|
|
232
401
|
android:layout_marginBottom="16dp"
|
|
233
|
-
app:cardCornerRadius="
|
|
234
|
-
app:cardElevation="
|
|
402
|
+
app:cardCornerRadius="16dp"
|
|
403
|
+
app:cardElevation="0dp"
|
|
404
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
405
|
+
app:strokeWidth="1dp">
|
|
235
406
|
|
|
236
407
|
<LinearLayout
|
|
237
408
|
android:layout_width="match_parent"
|
|
238
409
|
android:layout_height="wrap_content"
|
|
239
410
|
android:orientation="vertical"
|
|
240
|
-
android:padding="
|
|
411
|
+
android:padding="20dp">
|
|
241
412
|
|
|
242
413
|
<TextView
|
|
243
414
|
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
244
415
|
android:layout_width="wrap_content"
|
|
245
416
|
android:layout_height="wrap_content"
|
|
246
|
-
android:
|
|
247
|
-
android:text="@string/developer_tools_crash_testing" />
|
|
248
|
-
|
|
249
|
-
<com.google.android.material.button.MaterialButton
|
|
250
|
-
android:id="@+id/throwTestExceptionButton"
|
|
251
|
-
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
252
|
-
android:layout_width="match_parent"
|
|
253
|
-
android:layout_height="wrap_content"
|
|
254
|
-
android:text="@string/developer_tools_throw_test_exception" />
|
|
255
|
-
</LinearLayout>
|
|
256
|
-
</com.google.android.material.card.MaterialCardView>
|
|
257
|
-
|
|
258
|
-
<com.google.android.material.card.MaterialCardView
|
|
259
|
-
android:layout_width="match_parent"
|
|
260
|
-
android:layout_height="wrap_content"
|
|
261
|
-
app:cardCornerRadius="12dp"
|
|
262
|
-
app:cardElevation="1dp">
|
|
263
|
-
|
|
264
|
-
<LinearLayout
|
|
265
|
-
android:layout_width="match_parent"
|
|
266
|
-
android:layout_height="wrap_content"
|
|
267
|
-
android:orientation="vertical"
|
|
268
|
-
android:padding="16dp">
|
|
417
|
+
android:text="@string/developer_tools_webview" />
|
|
269
418
|
|
|
270
419
|
<TextView
|
|
271
|
-
style="@style/TextAppearance.MaterialComponents.
|
|
272
|
-
android:layout_width="
|
|
420
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
421
|
+
android:layout_width="match_parent"
|
|
273
422
|
android:layout_height="wrap_content"
|
|
423
|
+
android:layout_marginTop="2dp"
|
|
274
424
|
android:layout_marginBottom="12dp"
|
|
275
|
-
android:text="@string/
|
|
425
|
+
android:text="@string/developer_tools_webview_description"
|
|
426
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
276
427
|
|
|
277
428
|
<LinearLayout
|
|
278
429
|
android:layout_width="match_parent"
|
|
279
430
|
android:layout_height="wrap_content"
|
|
280
431
|
android:layout_marginBottom="12dp"
|
|
281
432
|
android:gravity="center_vertical"
|
|
433
|
+
android:minHeight="48dp"
|
|
282
434
|
android:orientation="horizontal">
|
|
283
435
|
|
|
284
436
|
<TextView
|
|
@@ -292,7 +444,8 @@
|
|
|
292
444
|
android:id="@+id/webViewStatus"
|
|
293
445
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
294
446
|
android:layout_width="wrap_content"
|
|
295
|
-
android:layout_height="wrap_content"
|
|
447
|
+
android:layout_height="wrap_content"
|
|
448
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
296
449
|
</LinearLayout>
|
|
297
450
|
|
|
298
451
|
<com.google.android.material.button.MaterialButton
|
|
@@ -303,6 +456,44 @@
|
|
|
303
456
|
android:text="@string/developer_tools_open_chrome_inspect_docs" />
|
|
304
457
|
</LinearLayout>
|
|
305
458
|
</com.google.android.material.card.MaterialCardView>
|
|
459
|
+
|
|
460
|
+
<com.google.android.material.card.MaterialCardView
|
|
461
|
+
android:layout_width="match_parent"
|
|
462
|
+
android:layout_height="wrap_content"
|
|
463
|
+
app:cardCornerRadius="16dp"
|
|
464
|
+
app:cardElevation="0dp"
|
|
465
|
+
app:strokeColor="?attr/colorControlNormal"
|
|
466
|
+
app:strokeWidth="1dp">
|
|
467
|
+
|
|
468
|
+
<LinearLayout
|
|
469
|
+
android:layout_width="match_parent"
|
|
470
|
+
android:layout_height="wrap_content"
|
|
471
|
+
android:orientation="vertical"
|
|
472
|
+
android:padding="20dp">
|
|
473
|
+
|
|
474
|
+
<TextView
|
|
475
|
+
style="@style/TextAppearance.MaterialComponents.Headline6"
|
|
476
|
+
android:layout_width="wrap_content"
|
|
477
|
+
android:layout_height="wrap_content"
|
|
478
|
+
android:text="@string/developer_tools_crash_testing" />
|
|
479
|
+
|
|
480
|
+
<TextView
|
|
481
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
482
|
+
android:layout_width="match_parent"
|
|
483
|
+
android:layout_height="wrap_content"
|
|
484
|
+
android:layout_marginTop="2dp"
|
|
485
|
+
android:layout_marginBottom="16dp"
|
|
486
|
+
android:text="@string/developer_tools_crash_testing_description"
|
|
487
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
488
|
+
|
|
489
|
+
<com.google.android.material.button.MaterialButton
|
|
490
|
+
android:id="@+id/throwTestExceptionButton"
|
|
491
|
+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
492
|
+
android:layout_width="match_parent"
|
|
493
|
+
android:layout_height="wrap_content"
|
|
494
|
+
android:text="@string/developer_tools_throw_test_exception" />
|
|
495
|
+
</LinearLayout>
|
|
496
|
+
</com.google.android.material.card.MaterialCardView>
|
|
306
497
|
</LinearLayout>
|
|
307
498
|
</androidx.core.widget.NestedScrollView>
|
|
308
499
|
</LinearLayout>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
<string name="app_name">Pakstr</string>
|
|
3
3
|
|
|
4
4
|
<string name="developer_tools_title">Developer Tools</string>
|
|
5
|
+
<string name="developer_tools_description">Inspect the app runtime, configure diagnostics, and collect information for troubleshooting.</string>
|
|
5
6
|
<string name="developer_tools_runtime">Runtime</string>
|
|
7
|
+
<string name="developer_tools_runtime_description">App, device, and session details</string>
|
|
6
8
|
<string name="developer_tools_session_id">Session ID</string>
|
|
7
9
|
<string name="developer_tools_app_version">App version</string>
|
|
8
10
|
<string name="developer_tools_android_version">Android version</string>
|
|
@@ -12,14 +14,29 @@
|
|
|
12
14
|
<string name="developer_tools_device_value">%1$s %2$s</string>
|
|
13
15
|
|
|
14
16
|
<string name="developer_tools_debug">Debug</string>
|
|
17
|
+
<string name="developer_tools_debug_description">Control diagnostic logging and inspection</string>
|
|
15
18
|
<string name="developer_tools_enable_debug_mode">Enable Debug Mode</string>
|
|
16
|
-
<string name="
|
|
17
|
-
<string name="
|
|
19
|
+
<string name="developer_tools_api_proxy">API Proxy</string>
|
|
20
|
+
<string name="developer_tools_api_proxy_description">Configure the backend used for same-origin /api/* requests.</string>
|
|
21
|
+
<string name="developer_tools_enable_api">API proxy enabled</string>
|
|
22
|
+
<string name="developer_tools_api_effective_status">Read-only status based on the effective API Base URL.</string>
|
|
23
|
+
<string name="developer_tools_api_effective_configuration">Effective configuration</string>
|
|
24
|
+
<string name="developer_tools_api_base_url">API Base URL</string>
|
|
25
|
+
<string name="developer_tools_api_base_runtime_override">Source: runtime override</string>
|
|
26
|
+
<string name="developer_tools_api_base_packaged_default">Source: packaged default</string>
|
|
27
|
+
<string name="developer_tools_api_base_not_configured">Source: not configured; API proxy disabled</string>
|
|
28
|
+
<string name="developer_tools_api_base_save">Save / Apply</string>
|
|
29
|
+
<string name="developer_tools_api_base_reset_button">Reset to default</string>
|
|
30
|
+
<string name="developer_tools_api_base_saved">API Base URL override applied.</string>
|
|
31
|
+
<string name="developer_tools_api_base_reset">Runtime override removed.</string>
|
|
32
|
+
<string name="developer_tools_api_base_save_failed">Unable to persist the API Base URL setting.</string>
|
|
33
|
+
<string name="developer_tools_api_base_invalid">Enter an absolute HTTPS URL without credentials, query, or fragment.</string>
|
|
18
34
|
<string name="developer_tools_webview_debug">WebView Debug</string>
|
|
19
35
|
<string name="developer_tools_enabled">Enabled</string>
|
|
20
36
|
<string name="developer_tools_disabled">Disabled</string>
|
|
21
37
|
|
|
22
|
-
<string name="developer_tools_logs">Logs</string>
|
|
38
|
+
<string name="developer_tools_logs">Logs & Reports</string>
|
|
39
|
+
<string name="developer_tools_logs_description">Review local diagnostics or export them for support</string>
|
|
23
40
|
<string name="developer_tools_view_logs">View Logs</string>
|
|
24
41
|
<string name="developer_tools_clear_logs">Clear Logs</string>
|
|
25
42
|
<string name="developer_tools_export_debug_report">Export Debug Report</string>
|
|
@@ -32,13 +49,15 @@
|
|
|
32
49
|
<string name="developer_tools_report_export_failed">Unable to export the debug report.</string>
|
|
33
50
|
|
|
34
51
|
<string name="developer_tools_crash_testing">Crash Testing</string>
|
|
52
|
+
<string name="developer_tools_crash_testing_description">Verify that uncaught errors are captured correctly</string>
|
|
35
53
|
<string name="developer_tools_throw_test_exception">Throw Test Exception</string>
|
|
36
54
|
<string name="developer_tools_crash_confirmation_title">Crash Pakstr?</string>
|
|
37
55
|
<string name="developer_tools_crash_confirmation_message">This intentionally throws an uncaught RuntimeException and closes the app so CrashHandler can capture it.</string>
|
|
38
56
|
<string name="developer_tools_crash">Crash</string>
|
|
39
57
|
|
|
40
|
-
<string name="developer_tools_webview">WebView</string>
|
|
41
|
-
<string name="
|
|
58
|
+
<string name="developer_tools_webview">WebView Inspection</string>
|
|
59
|
+
<string name="developer_tools_webview_description">Inspect rendered web content with Chrome DevTools</string>
|
|
60
|
+
<string name="developer_tools_current_status">Inspection status</string>
|
|
42
61
|
<string name="developer_tools_open_chrome_inspect_docs">Open Chrome Inspect documentation</string>
|
|
43
62
|
<string name="developer_tools_browser_unavailable">No browser is available.</string>
|
|
44
63
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class PakstrBridgeRuntimeApiBaseTest {
|
|
9
|
+
@Test
|
|
10
|
+
fun setterPersistsValidatedOverrideAndGetterReturnsEffectiveValue() {
|
|
11
|
+
val preferences = FakePreferences()
|
|
12
|
+
val bridge = PakstrBridge(
|
|
13
|
+
RuntimeApiBaseSettings.createForTesting(preferences) {
|
|
14
|
+
"https://default.example.com"
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assertTrue(bridge.setApiBaseUrl("https://override.example.com/"))
|
|
19
|
+
assertEquals("https://override.example.com", bridge.getApiBaseUrl())
|
|
20
|
+
assertFalse(bridge.setApiBaseUrl("http://unsafe.example.com"))
|
|
21
|
+
assertEquals("https://override.example.com", bridge.getApiBaseUrl())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun setterReportsPersistenceFailure() {
|
|
26
|
+
val bridge = PakstrBridge(
|
|
27
|
+
RuntimeApiBaseSettings.createForTesting(FakePreferences(commitResult = false)) {
|
|
28
|
+
null
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assertFalse(bridge.setApiBaseUrl("https://api.example.com"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private class FakePreferences(
|
|
36
|
+
private val commitResult: Boolean = true
|
|
37
|
+
) : RuntimeApiBaseSettings.Preferences {
|
|
38
|
+
private val values = mutableMapOf<String, String>()
|
|
39
|
+
|
|
40
|
+
override fun getString(key: String): String? = values[key]
|
|
41
|
+
|
|
42
|
+
override fun putString(key: String, value: String): Boolean {
|
|
43
|
+
if (commitResult) values[key] = value
|
|
44
|
+
return commitResult
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun remove(key: String): Boolean {
|
|
48
|
+
if (commitResult) values.remove(key)
|
|
49
|
+
return commitResult
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class RuntimeApiBaseProxyTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun proxyUsesUpdatedEffectiveValueOnSubsequentRequest() {
|
|
10
|
+
val preferences = FakePreferences()
|
|
11
|
+
val settings = RuntimeApiBaseSettings.createForTesting(preferences) {
|
|
12
|
+
"https://default.example.com"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
val firstRequestApiBase = settings.current().url!!
|
|
16
|
+
assertEquals(
|
|
17
|
+
"https://default.example.com/api/items",
|
|
18
|
+
proxyApiUrl(firstRequestApiBase, "/api/items").toString()
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assertTrue(settings.setOverride("https://updated.example.com/base/"))
|
|
22
|
+
|
|
23
|
+
val secondRequestApiBase = settings.current().url!!
|
|
24
|
+
assertEquals(
|
|
25
|
+
"https://updated.example.com/base/api/items",
|
|
26
|
+
proxyApiUrl(secondRequestApiBase, "/api/items").toString()
|
|
27
|
+
)
|
|
28
|
+
assertEquals(
|
|
29
|
+
"https://default.example.com/api/items",
|
|
30
|
+
proxyApiUrl(firstRequestApiBase, "/api/items").toString()
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private class FakePreferences : RuntimeApiBaseSettings.Preferences {
|
|
35
|
+
private val values = mutableMapOf<String, String>()
|
|
36
|
+
|
|
37
|
+
override fun getString(key: String): String? = values[key]
|
|
38
|
+
|
|
39
|
+
override fun putString(key: String, value: String): Boolean {
|
|
40
|
+
values[key] = value
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun remove(key: String): Boolean {
|
|
45
|
+
values.remove(key)
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class RuntimeApiBaseSettingsTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun persistedOverrideTakesPrecedenceOverPackagedDefault() {
|
|
12
|
+
val preferences = FakePreferences()
|
|
13
|
+
val settings = settings(preferences, "https://default.example.com")
|
|
14
|
+
|
|
15
|
+
assertTrue(settings.setOverride("https://override.example.com/"))
|
|
16
|
+
assertEquals(
|
|
17
|
+
"https://override.example.com",
|
|
18
|
+
preferences.values[RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY]
|
|
19
|
+
)
|
|
20
|
+
assertEquals(
|
|
21
|
+
RuntimeApiBaseSettings.Value(
|
|
22
|
+
"https://override.example.com",
|
|
23
|
+
RuntimeApiBaseSettings.Source.RUNTIME_OVERRIDE
|
|
24
|
+
),
|
|
25
|
+
settings.current()
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun resetRemovesOverrideAndRestoresPackagedDefault() {
|
|
31
|
+
val preferences = FakePreferences()
|
|
32
|
+
val settings = settings(preferences, "https://default.example.com/")
|
|
33
|
+
settings.setOverride("https://override.example.com")
|
|
34
|
+
|
|
35
|
+
assertTrue(settings.reset())
|
|
36
|
+
assertFalse(preferences.values.containsKey(RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY))
|
|
37
|
+
assertEquals(
|
|
38
|
+
RuntimeApiBaseSettings.Value(
|
|
39
|
+
"https://default.example.com",
|
|
40
|
+
RuntimeApiBaseSettings.Source.PACKAGED_DEFAULT
|
|
41
|
+
),
|
|
42
|
+
settings.current()
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun noConfiguredDefaultDisablesApiProxy() {
|
|
48
|
+
val settings = settings(FakePreferences(), null)
|
|
49
|
+
|
|
50
|
+
assertEquals(
|
|
51
|
+
RuntimeApiBaseSettings.Value(
|
|
52
|
+
null,
|
|
53
|
+
RuntimeApiBaseSettings.Source.DISABLED
|
|
54
|
+
),
|
|
55
|
+
settings.current()
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun invalidPersistedValueIsIgnored() {
|
|
61
|
+
val preferences = FakePreferences(
|
|
62
|
+
mutableMapOf(
|
|
63
|
+
RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY to "http://unsafe.example.com"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assertEquals(
|
|
68
|
+
"https://default.example.com",
|
|
69
|
+
settings(preferences, "https://default.example.com").current().url
|
|
70
|
+
)
|
|
71
|
+
assertNull(settings(preferences, null).current().url)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
fun invalidOverrideDoesNotPersist() {
|
|
76
|
+
val preferences = FakePreferences()
|
|
77
|
+
val settings = settings(preferences, "https://default.example.com")
|
|
78
|
+
|
|
79
|
+
assertFalse(settings.setOverride("https://user@example.com"))
|
|
80
|
+
assertTrue(preferences.values.isEmpty())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun settings(
|
|
84
|
+
preferences: FakePreferences,
|
|
85
|
+
packagedDefault: String?
|
|
86
|
+
): RuntimeApiBaseSettings = RuntimeApiBaseSettings.createForTesting(
|
|
87
|
+
preferences
|
|
88
|
+
) {
|
|
89
|
+
packagedDefault
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private class FakePreferences(
|
|
93
|
+
val values: MutableMap<String, String> = mutableMapOf()
|
|
94
|
+
) : RuntimeApiBaseSettings.Preferences {
|
|
95
|
+
override fun getString(key: String): String? = values[key]
|
|
96
|
+
|
|
97
|
+
override fun putString(key: String, value: String): Boolean {
|
|
98
|
+
values[key] = value
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun remove(key: String): Boolean {
|
|
103
|
+
values.remove(key)
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class RuntimeApiBaseValidatorTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun acceptsAbsoluteHttpsUrlWithHostname() {
|
|
10
|
+
assertEquals(
|
|
11
|
+
"https://api.example.com/v1",
|
|
12
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
13
|
+
"https://api.example.com/v1"
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun normalizesTrailingSlashesAndHostCase() {
|
|
20
|
+
assertEquals(
|
|
21
|
+
"https://api.example.com/v1",
|
|
22
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
23
|
+
"HTTPS://API.Example.COM/v1///"
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
assertEquals(
|
|
27
|
+
"https://api.example.com",
|
|
28
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
29
|
+
"https://api.example.com/"
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun rejectsInvalidOrUnsafeUrls() {
|
|
36
|
+
listOf(
|
|
37
|
+
"",
|
|
38
|
+
" https://api.example.com",
|
|
39
|
+
"http://api.example.com",
|
|
40
|
+
"api.example.com",
|
|
41
|
+
"https:///v1",
|
|
42
|
+
"https://user:password@api.example.com",
|
|
43
|
+
"https://api.example.com?v=1",
|
|
44
|
+
"https://api.example.com#fragment",
|
|
45
|
+
"https://api.example.com/${"a".repeat(RuntimeApiBaseValidator.MAX_LENGTH)}"
|
|
46
|
+
).forEach { value ->
|
|
47
|
+
assertNull(value, RuntimeApiBaseValidator.validateAndCanonicalize(value))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|