Skip to content

Commit bd0581d

Browse files
oschwaldclaude
andcommitted
feat: Add local development server support for sample app
Add ability to configure the sample app to connect to a local development server instead of production MaxMind servers. This is useful for testing the SDK against a local backend. Configuration via local.properties (gitignored): - debug.server.url: Custom server URL (e.g., https://localhost:8443) - debug.ca.cert: Path to CA certificate for self-signed HTTPS When debug.ca.cert is configured, the build system: - Copies the certificate to res/raw/debug_ca.crt (gitignored) - Generates a network_security_config.xml that trusts the bundled cert - Sets BuildConfig.DEBUG_SERVER_URL for the app to use Without configuration, the sample app connects to production servers. Also documents ADB reverse port forwarding for physical device testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f224769 commit bd0581d

File tree

6 files changed

+198
-2
lines changed

6 files changed

+198
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ inputs.csv
102102
summary.md
103103
plan.md
104104
build-log.txt
105+
106+
# Generated debug CA certificate (configured via local.properties)
107+
sample/src/main/res/raw/debug_ca.crt

SETUP.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,98 @@ device-android/
188188
└── README.md # Main documentation
189189
```
190190

191+
## Local Development Server
192+
193+
The sample app can be configured to connect to a local development server
194+
instead of the production MaxMind servers. This is useful for testing the SDK
195+
against a local backend.
196+
197+
### Quick Start
198+
199+
1. **Add to `local.properties`**:
200+
201+
```properties
202+
debug.server.url=https://localhost:8443
203+
debug.ca.cert=/path/to/your/ca.crt
204+
```
205+
206+
2. **Set up ADB reverse port forwarding**:
207+
208+
```bash
209+
adb reverse tcp:8443 tcp:8443
210+
```
211+
212+
3. **Build and install**:
213+
214+
```bash
215+
./gradlew :sample:installDebug
216+
```
217+
218+
### Configuration Options
219+
220+
Add these to `local.properties` (gitignored):
221+
222+
| Property | Description |
223+
| ------------------ | -------------------------------------------- |
224+
| `debug.server.url` | Server URL (e.g., `https://localhost:8443`) |
225+
| `debug.ca.cert` | Path to CA certificate for self-signed HTTPS |
226+
227+
Both properties are optional. Without them, the sample app connects to
228+
production MaxMind servers.
229+
230+
### ADB Reverse Port Forwarding
231+
232+
When running the sample app on a physical device, `localhost` on the device
233+
refers to the device itself, not your development machine. ADB reverse creates a
234+
tunnel:
235+
236+
```bash
237+
# Forward device's localhost:8443 to your machine's localhost:8443
238+
adb reverse tcp:8443 tcp:8443
239+
240+
# Verify the forwarding is active
241+
adb reverse --list
242+
243+
# Remove forwarding when done
244+
adb reverse --remove tcp:8443
245+
```
246+
247+
**Note:** You must re-run `adb reverse` each time you reconnect the device.
248+
249+
For emulators, you can alternatively use `10.0.2.2` which automatically routes
250+
to the host machine's localhost.
251+
252+
### How Certificate Bundling Works
253+
254+
When `debug.ca.cert` is configured and the file exists, the build system:
255+
256+
1. Copies the certificate to `sample/src/main/res/raw/debug_ca.crt` (gitignored)
257+
2. Generates a `network_security_config.xml` that trusts the bundled certificate
258+
3. Sets `BuildConfig.DEBUG_SERVER_URL` for the app to use
259+
260+
When not configured, the sample app behaves normally with the default network
261+
security config.
262+
263+
### Troubleshooting
264+
265+
**"Trust anchor for certification path not found"**
266+
267+
- Ensure `debug.ca.cert` points to a valid CA certificate file (not a leaf cert)
268+
- Rebuild: `./gradlew :sample:clean :sample:installDebug`
269+
- Verify the certificate was copied: `ls sample/src/main/res/raw/`
270+
271+
**Connection refused / timeout**
272+
273+
- Verify ADB reverse is active: `adb reverse --list`
274+
- Ensure your local server is running on the correct port
275+
- Check that the server binds to `0.0.0.0` or `localhost`
276+
277+
**Changes not taking effect**
278+
279+
- Run a clean build: `./gradlew :sample:clean :sample:installDebug`
280+
- The network security config is generated at build time based on
281+
`local.properties`
282+
191283
## Support
192284

193285
If you encounter issues:

sample/build.gradle.kts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.util.Properties
2+
13
plugins {
24
alias(libs.plugins.android.application)
35
alias(libs.plugins.kotlin.android)
@@ -6,6 +8,15 @@ plugins {
68
alias(libs.plugins.ktlint)
79
}
810

11+
// Read local.properties for debug configuration
12+
val localPropertiesFile = rootProject.file("local.properties")
13+
val localProperties = Properties()
14+
if (localPropertiesFile.exists()) {
15+
localProperties.load(localPropertiesFile.inputStream())
16+
}
17+
val debugServerUrl = localProperties.getProperty("debug.server.url", "")
18+
val debugCaCertPath = localProperties.getProperty("debug.ca.cert", "")
19+
920
android {
1021
namespace = "com.maxmind.device.sample"
1122
compileSdk =
@@ -27,6 +38,8 @@ android {
2738
versionName = "1.0"
2839

2940
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
41+
42+
buildConfigField("String", "DEBUG_SERVER_URL", "\"$debugServerUrl\"")
3043
}
3144

3245
buildTypes {
@@ -53,6 +66,7 @@ android {
5366

5467
buildFeatures {
5568
viewBinding = true
69+
buildConfig = true
5670
}
5771
}
5872

@@ -85,3 +99,64 @@ detekt {
8599
config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
86100
buildUponDefaultConfig = true
87101
}
102+
103+
// Debug CA certificate support for local development
104+
// Usage: Add to local.properties:
105+
// debug.ca.cert=/path/to/your/ca.crt
106+
// debug.server.url=https://localhost:8443
107+
val hasDebugCert = debugCaCertPath.isNotEmpty() && file(debugCaCertPath).exists()
108+
109+
if (hasDebugCert) {
110+
// Copy certificate to res/raw
111+
tasks.register<Copy>("copyDebugCaCert") {
112+
from(debugCaCertPath)
113+
into("src/main/res/raw")
114+
rename { "debug_ca.crt" }
115+
}
116+
117+
// Generate network security config that includes the bundled cert
118+
tasks.register("generateNetworkSecurityConfig") {
119+
dependsOn("copyDebugCaCert")
120+
val outputFile = file("src/main/res/xml/network_security_config.xml")
121+
outputs.file(outputFile)
122+
doLast {
123+
outputFile.parentFile.mkdirs()
124+
outputFile.writeText(
125+
"""
126+
|<?xml version="1.0" encoding="utf-8"?>
127+
|<!-- Generated - do not edit. Configure via local.properties -->
128+
|<network-security-config>
129+
| <debug-overrides>
130+
| <trust-anchors>
131+
| <certificates src="@raw/debug_ca" />
132+
| <certificates src="user" />
133+
| <certificates src="system" />
134+
| </trust-anchors>
135+
| </debug-overrides>
136+
| <domain-config>
137+
| <domain includeSubdomains="false">localhost</domain>
138+
| <trust-anchors>
139+
| <certificates src="@raw/debug_ca" />
140+
| <certificates src="user" />
141+
| <certificates src="system" />
142+
| </trust-anchors>
143+
| </domain-config>
144+
|</network-security-config>
145+
""".trimMargin(),
146+
)
147+
}
148+
}
149+
150+
tasks.named("preBuild") {
151+
dependsOn("generateNetworkSecurityConfig")
152+
}
153+
154+
// Clean up generated files
155+
tasks.named("clean") {
156+
doLast {
157+
delete("src/main/res/raw/debug_ca.crt")
158+
}
159+
}
160+
} else if (debugCaCertPath.isNotEmpty()) {
161+
logger.warn("Debug CA certificate not found at: $debugCaCertPath")
162+
}

sample/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<application
55
android:allowBackup="false"
66
android:label="@string/app_name"
7+
android:networkSecurityConfig="@xml/network_security_config"
78
android:supportsRtl="true"
89
android:theme="@style/Theme.DeviceTrackerSample">
910
<activity

sample/src/main/java/com/maxmind/device/sample/MainActivity.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,17 @@ class MainActivity : AppCompatActivity() {
6969

7070
// Create SDK configuration
7171
// Note: Replace with your actual MaxMind account ID
72-
val config =
72+
val configBuilder =
7373
SdkConfig
7474
.Builder(123456) // Demo account ID - replace with real one
7575
.enableLogging(true)
76-
.build()
76+
77+
// Use debug server URL if configured in local.properties
78+
if (BuildConfig.DEBUG_SERVER_URL.isNotEmpty()) {
79+
configBuilder.serverUrl(BuildConfig.DEBUG_SERVER_URL)
80+
}
81+
82+
val config = configBuilder.build()
7783

7884
// Initialize SDK
7985
DeviceTracker.initialize(this, config)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Generated - do not edit. Configure via local.properties -->
3+
<network-security-config>
4+
<debug-overrides>
5+
<trust-anchors>
6+
<certificates src="@raw/debug_ca" />
7+
<certificates src="user" />
8+
<certificates src="system" />
9+
</trust-anchors>
10+
</debug-overrides>
11+
<domain-config>
12+
<domain includeSubdomains="false">localhost</domain>
13+
<trust-anchors>
14+
<certificates src="@raw/debug_ca" />
15+
<certificates src="user" />
16+
<certificates src="system" />
17+
</trust-anchors>
18+
</domain-config>
19+
</network-security-config>

0 commit comments

Comments
 (0)