Debug Workflow
Debug, test, and profile Android applications using adb, emulator, logcat, and testing frameworks.
ADB (Android Debug Bridge)
Basic Commands
# List connected devices
adb devices
# Connect to specific device (if multiple)
adb -s <device-id> <command>
# Target emulator specifically
adb -e <command>
# Target physical device specifically
adb -d <command>
# Check device info
adb shell getprop ro.product.model
adb shell getprop ro.build.version.release
App Management
# Install APK
adb install app-debug.apk
# Reinstall (keep data)
adb install -r app-debug.apk
# Uninstall app
adb uninstall com.example.myapp
# Clear app data
adb shell pm clear com.example.myapp
# List installed packages
adb shell pm list packages
adb shell pm list packages | grep myapp
# Get app info
adb shell dumpsys package com.example.myapp
Start/Stop App
# Start app activity
adb shell am start -n com.example.myapp/.MainActivity
# Start with intent data
adb shell am start -n com.example.myapp/.MainActivity -d "https://example.com"
# Stop (force close) app
adb shell am force-stop com.example.myapp
# Kill app process
adb shell am kill com.example.myapp
File Operations
# Push file to device
adb push local-file.txt /sdcard/
# Pull file from device
adb pull /sdcard/file.txt ./
# List files
adb shell ls /sdcard/
# View file content
adb shell cat /sdcard/file.txt
# Remove file
adb shell rm /sdcard/file.txt
Screenshots and Screen Recording
# Take screenshot
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png
adb shell rm /sdcard/screen.png
# Or in one command
adb exec-out screencap -p > screen.png
# Record screen (max 3 minutes)
adb shell screenrecord /sdcard/demo.mp4
# Press Ctrl+C to stop
adb pull /sdcard/demo.mp4
adb shell rm /sdcard/demo.mp4
System Information
# CPU info
adb shell cat /proc/cpuinfo
# Memory info
adb shell cat /proc/meminfo
# Battery status
adb shell dumpsys battery
# Display info
adb shell dumpsys display
# Running processes
adb shell ps
# App processes
adb shell ps | grep com.example.myapp
Network
# Check network connectivity
adb shell ping -c 4 google.com
# View network interfaces
adb shell ip addr
# Port forwarding (device port to local port)
adb forward tcp:8080 tcp:8080
# Reverse port forwarding (local port to device)
adb reverse tcp:9000 tcp:9000
Logcat
View Logs
# All logs (verbose)
adb logcat
# Clear logs first, then follow
adb logcat -c && adb logcat
# Filter by app package
adb logcat | grep "com.example.myapp"
# Filter by tag
adb logcat -s MyTag
# Filter by priority (V=Verbose, D=Debug, I=Info, W=Warn, E=Error, F=Fatal)
adb logcat *:E # Only errors
# Multiple filters
adb logcat MyTag:D *:E # MyTag at Debug level, everything else at Error
Log Priorities
V - Verbose (lowest priority)
D - Debug
I - Info
W - Warning
E - Error
F - Fatal
S - Silent (highest priority, nothing printed)
Format Options
# Brief format (default)
adb logcat -v brief
# Time format (with timestamps)
adb logcat -v time
# Threadtime (time + PID + TID)
adb logcat -v threadtime
# Long format (all metadata)
adb logcat -v long
Save Logs to File
# Save all logs
adb logcat > logs.txt
# Save with timestamp in filename
adb logcat > "logs-$(date +%Y%m%d-%H%M%S).txt"
# Save for 10 seconds then stop
timeout 10s adb logcat > logs.txt
Filter Crash Logs
# Show only fatal errors
adb logcat *:F
# Show crashes and errors
adb logcat *:E
# Find specific crash
adb logcat | grep -A 50 "FATAL EXCEPTION"
# Show stack traces
adb logcat | grep -E "(at |Caused by)"
Gomobile / Go-specific Logs
# Go runtime logs
adb logcat | grep GoLog
# Go panic messages
adb logcat | grep "panic:"
# Your Go log.Printf output
adb logcat | grep "GoLog"
Emulator
System Images
Image Types:
| Type | Google Apps | Play Store | Use Case |
|---|---|---|---|
default |
❌ | ❌ | AOSP, minimal |
google_apis |
✅ | ❌ | Google Maps, etc. |
google_apis_playstore |
✅ | ✅ | Full Play Services |
Install System Images:
# List available system images
sdkmanager --list | grep system-images
# Android 14 (API 34) - x86_64 (for Intel/AMD)
sdkmanager "system-images;android-34;google_apis;x86_64"
# Android 14 (API 34) - ARM64 (for Apple Silicon)
sdkmanager "system-images;android-34;google_apis;arm64-v8a"
# Android 13 (API 33)
sdkmanager "system-images;android-33;google_apis;x86_64"
Choose architecture:
- Intel/AMD processors:
x86_64 - Apple Silicon (M1/M2/M3):
arm64-v8a
Create AVD
# Quick start
avdmanager create avd \
-n Pixel7 \
-k "system-images;android-34;google_apis;x86_64" \
-d "pixel_7"
# List available device definitions
avdmanager list device
# Common devices: pixel_7, pixel_7_pro, pixel_6, pixel_5, pixel_tablet
Examples:
# Pixel 6 with Android 13
avdmanager create avd \
-n Pixel6 \
-k "system-images;android-33;google_apis;x86_64" \
-d "pixel_6"
# Tablet (Pixel Tablet)
avdmanager create avd \
-n PixelTablet \
-k "system-images;android-34;google_apis;x86_64" \
-d "pixel_tablet"
# Custom configuration
avdmanager create avd \
-n CustomPhone \
-k "system-images;android-34;google_apis;x86_64" \
-d "pixel_7" \
-c 512M \ # SD card size
--force # Overwrite if exists
Manage AVDs
# List all AVDs
avdmanager list avd
# List with paths
avdmanager list avd -c
# Delete AVD
avdmanager delete avd -n Pixel7
Start Emulator
Basic:
# Start AVD
emulator -avd Pixel7
# Start in background
emulator -avd Pixel7 &
# Start with GPU acceleration
emulator -avd Pixel7 -gpu host
Common Options:
# Wipe data (factory reset)
emulator -avd Pixel7 -wipe-data
# Read-only system
emulator -avd Pixel7 -read-only
# Increase RAM
emulator -avd Pixel7 -memory 4096
# Cold boot (ignore snapshot)
emulator -avd Pixel7 -no-snapshot-load
# No audio
emulator -avd Pixel7 -no-audio
# Maximum performance
emulator -avd Pixel7 \
-gpu host \
-memory 4096 \
-cores 4 \
-no-boot-anim \
-no-snapshot
Network Options:
# Use specific DNS
emulator -avd Pixel7 -dns-server 8.8.8.8
# Network latency simulation
emulator -avd Pixel7 -netdelay gprs # or edge, umts, none
# Network speed simulation
emulator -avd Pixel7 -netspeed full # or gsm, hscsd, gprs, edge, umts, hsdpa
# Proxy
emulator -avd Pixel7 -http-proxy http://localhost:8888
Emulator Control
Via Telnet:
# Connect to emulator console
telnet localhost 5554 # Port shown when emulator starts
# In telnet session:
# auth <token> # Token is in ~/.emulator_console_auth_token
# help
# sms send 1234567890 "Test message"
# geo fix -122.084 37.422 # Set GPS location
# quit
Via ADB:
# Rotate screen
adb shell settings put system accelerometer_rotation 0
adb shell settings put system user_rotation 1 # 0=0°, 1=90°, 2=180°, 3=270°
# Simulate low battery
adb shell dumpsys battery set level 10
# Reset battery
adb shell dumpsys battery reset
Multiple Emulators
# Start first emulator
emulator -avd Pixel7 &
# Start second emulator (different AVD)
emulator -avd Pixel6 &
# Install on specific emulator
adb -s emulator-5554 install app-debug.apk
adb -s emulator-5556 install app-debug.apk
Emulator Ports:
- First emulator: console 5554, adb 5555
- Second emulator: console 5556, adb 5557
Testing
Test Types
| Type | Runs On | Speed | Use For |
|---|---|---|---|
| Unit Tests | JVM | Fast | Business logic, utilities |
| Instrumentation Tests | Android device/emulator | Slow | Android APIs, database |
| UI Tests | Android device/emulator | Slowest | User interface, interactions |
Unit Tests (Local Tests)
Location: app/src/test/java/com/example/myapp/
Dependencies:
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.7.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
}
Example:
package com.example.myapp
import org.junit.Test
import org.junit.Assert.*
class CalculatorTest {
@Test
fun addition_isCorrect() {
val calculator = Calculator()
val result = calculator.add(2, 3)
assertEquals(5, result)
}
@Test
fun division_byZero_throwsException() {
val calculator = Calculator()
assertThrows(ArithmeticException::class.java) {
calculator.divide(10, 0)
}
}
}
Run Unit Tests:
# All unit tests
./gradlew test
# Specific variant
./gradlew testDebug
# Specific test class
./gradlew test --tests CalculatorTest
# With coverage report
./gradlew testDebugUnitTest jacocoTestReport
View Results:
# HTML report
open app/build/reports/tests/testDebugUnitTest/index.html
# Coverage report
open app/build/reports/jacoco/jacocoTestReport/html/index.html
Instrumentation Tests
Location: app/src/androidTest/java/com/example/myapp/
Dependencies:
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
Example:
package com.example.myapp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class DatabaseTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.myapp", appContext.packageName)
}
}
Run Instrumentation Tests:
# Connect device/emulator first
adb devices
# All instrumentation tests
./gradlew connectedAndroidTest
# Specific variant
./gradlew connectedDebugAndroidTest
# Specific test
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.myapp.DatabaseTest
UI Tests (Espresso)
Dependencies:
dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
}
Example:
package com.example.myapp
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun button_click_showsText() {
// Type text in EditText
onView(withId(R.id.editText))
.perform(typeText("Hello"), closeSoftKeyboard())
// Click button
onView(withId(R.id.button))
.perform(click())
// Verify text is displayed
onView(withId(R.id.textView))
.check(matches(withText("Hello")))
}
}
Common Espresso Operations:
// Find views
onView(withId(R.id.viewId))
onView(withText("text"))
onView(withContentDescription("description"))
// Actions
perform(click())
perform(typeText("text"))
perform(replaceText("new text"))
perform(clearText())
perform(closeSoftKeyboard())
perform(swipeLeft())
perform(swipeRight())
perform(scrollTo())
// Assertions
check(matches(isDisplayed()))
check(matches(withText("expected")))
check(matches(isEnabled()))
check(matches(isChecked()))
check(doesNotExist())
Testing Gomobile Integration
Unit Test Go Code (Before binding):
cd golib
go test ./...
go test -v ./...
go test -cover ./...
Test AAR Integration:
package com.example.myapp
import androidx.test.ext.junit.runners.AndroidJUnit4
import mylib.Mylib // Import gomobile generated package
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class GomobileTest {
@Test
fun goFunction_returnsExpectedResult() {
val result = Mylib.fetchURL("https://example.com")
assertNotNull(result)
assertTrue(result.isNotEmpty())
}
@Test
fun goFunction_handlesError() {
try {
Mylib.fetchURL("invalid-url")
fail("Should have thrown exception")
} catch (e: Exception) {
assertTrue(e.message?.contains("error") == true)
}
}
}
Code Coverage (JaCoCo)
Configure:
plugins {
id("jacoco")
}
android {
buildTypes {
debug {
enableAndroidTestCoverage = true
enableUnitTestCoverage = true
}
}
}
tasks.register<JacocoReport>("jacocoTestReport") {
dependsOn("testDebugUnitTest", "createDebugCoverageReport")
reports {
xml.required.set(true)
html.required.set(true)
}
val fileFilter = listOf(
"**/R.class",
"**/R$*.class",
"**/BuildConfig.*",
"**/Manifest*.*"
)
val debugTree = fileTree("${project.buildDir}/intermediates/javac/debug") {
exclude(fileFilter)
}
val mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories.setFrom(files(mainSrc))
classDirectories.setFrom(files(debugTree))
executionData.setFrom(fileTree(project.buildDir) {
include("jacoco/testDebugUnitTest.exec", "outputs/code_coverage/debugAndroidTest/connected/**/*.ec")
})
}
Run Coverage:
./gradlew jacocoTestReport
open app/build/reports/jacoco/jacocoTestReport/html/index.html
Test Best Practices
1. Follow AAA Pattern:
@Test
fun testName() {
// Arrange - Set up test data
val calculator = Calculator()
// Act - Execute the function
val result = calculator.add(2, 3)
// Assert - Verify the result
assertEquals(5, result)
}
2. Use Descriptive Names:
// Good
@Test
fun addition_withPositiveNumbers_returnsSum() { }
@Test
fun division_withZeroDivisor_throwsException() { }
// Bad
@Test
fun test1() { }
3. Test One Thing:
// Good - separate tests
@Test
fun add_returnsCorrectSum() { }
@Test
fun add_handlesNegativeNumbers() { }
4. Mock External Dependencies:
import org.mockito.Mock
import org.mockito.Mockito.*
class UserRepositoryTest {
@Mock
lateinit var api: ApiService
@Test
fun getUser_callsApi() {
val userId = "123"
`when`(api.fetchUser(userId)).thenReturn(User("John"))
val repository = UserRepository(api)
val user = repository.getUser(userId)
verify(api).fetchUser(userId)
assertEquals("John", user.name)
}
}
Debugging from Code
Android (Kotlin/Java)
import android.util.Log
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Log levels
Log.v(TAG, "Verbose message")
Log.d(TAG, "Debug message")
Log.i(TAG, "Info message")
Log.w(TAG, "Warning message")
Log.e(TAG, "Error message")
// With exception
try {
// Code that might throw
} catch (e: Exception) {
Log.e(TAG, "Error occurred", e)
}
}
}
View in logcat:
adb logcat -s MainActivity
Go Code (Gomobile)
package mylib
import "log"
func FetchData(url string) (string, error) {
log.Printf("Fetching URL: %s", url) // Appears in logcat with GoLog tag
result, err := doFetch(url)
if err != nil {
log.Printf("Error fetching: %v", err)
return "", err
}
log.Printf("Fetch successful, got %d bytes", len(result))
return result, nil
}
View in logcat:
adb logcat | grep GoLog
Interactive Debugging
Enable Debug Mode:
// app/build.gradle.kts
android {
buildTypes {
debug {
isDebuggable = true
}
}
}
Using Android Studio Debugger:
- Build debug APK:
./gradlew assembleDebug - Install on device:
adb install app-debug.apk - In Android Studio: Run → Attach Debugger to Android Process
- Select your app
- Set breakpoints in code
- Trigger the code path
Performance Profiling
CPU Profiling
# Start profiling
adb shell am profile start com.example.myapp /sdcard/profile.trace
# Stop profiling
adb shell am profile stop com.example.myapp
# Pull trace file
adb pull /sdcard/profile.trace
# Analyze with Android Studio or Perfetto
Memory Profiling
# Get memory info for app
adb shell dumpsys meminfo com.example.myapp
# Heap dump
adb shell am dumpheap com.example.myapp /sdcard/heap.hprof
adb pull /sdcard/heap.hprof
# Analyze with Android Studio Memory Profiler
Monitor App Performance
# CPU usage
adb shell top | grep com.example.myapp
# Memory usage over time
adb shell dumpsys meminfo com.example.myapp | grep TOTAL
Debugging Crashes
1. View Crash Logs
adb logcat -c # Clear old logs
# Reproduce crash
adb logcat *:E # View errors
2. Find Stack Trace
Look for:
FATAL EXCEPTION: main
Process: com.example.myapp, PID: 12345
java.lang.NullPointerException: Attempt to invoke virtual method '...' on a null object reference
at com.example.myapp.MainActivity.onCreate(MainActivity.kt:25)
at android.app.Activity.performCreate(Activity.java:...)
3. Analyze Stack Trace
- Line number:
MainActivity.kt:25- exact location - Exception type:
NullPointerException- what went wrong - Message: Details about the error
- Call stack: How we got there
4. Common Crash Types
NullPointerException:
// Bad
val user = getUser() // Returns null
val name = user.name // Crash!
// Good
val user = getUser()
val name = user?.name ?: "Unknown"
ClassCastException:
// Bad
val text = view as TextView // Crash if not TextView!
// Good
val text = view as? TextView
ActivityNotFoundException:
// Check if intent can be handled
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
Network Debugging
HTTP Traffic Inspection
# Use adb reverse to connect app to local proxy
adb reverse tcp:8888 tcp:8888
# App should connect to localhost:8888
# Proxy (mitmproxy, Charles, Burp Suite) runs on port 8888
View Network Stats
adb shell dumpsys netstats
Troubleshooting
ADB Issues
“adb: device offline”:
adb kill-server
adb start-server
# Reconnect device
“adb: device unauthorized”:
- Check device screen for authorization prompt
- Accept “Always allow from this computer”
- If no prompt:
adb kill-server rm ~/.android/adbkey* adb start-server
“Logcat shows nothing”:
# Clear buffer
adb logcat -c
# Check buffer size
adb logcat -g
# Increase buffer size
adb logcat -G 16M
Emulator Issues
Emulator won’t start:
# Check emulator location
which emulator
# Run with verbose logging
emulator -avd Pixel7 -verbose
# Check for errors
emulator -avd Pixel7 -debug init
Slow emulator - Enable hardware acceleration:
Linux (KVM):
# Check KVM support
egrep -c '(vmx|svm)' /proc/cpuinfo # Should be > 0
# Check KVM device
ls -l /dev/kvm
# Add user to kvm group
sudo usermod -a -G kvm $USER
# Log out and back in
# Verify
emulator -accel-check
macOS: Automatically uses Hypervisor Framework
Windows (HAXM):
sdkmanager "extras;intel;Hardware_Accelerated_Execution_Manager"
Graphics issues:
# Try different GPU modes
emulator -avd Pixel7 -gpu auto
emulator -avd Pixel7 -gpu host
emulator -avd Pixel7 -gpu swiftshader_indirect
emulator -avd Pixel7 -gpu angle_indirect
Test Issues
Tests fail on CI but pass locally:
- Check Android API level compatibility
- Verify emulator configuration
- Check for timing issues (add timeouts)
- Ensure deterministic test data
Instrumentation tests hang:
# Clear app data between tests
adb shell pm clear com.example.myapp
# Restart adb
adb kill-server
adb start-server
Espresso can’t find view:
// Use idling resources for async operations
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
Helper Scripts
Monitor App Script
Create monitor-app.sh:
#!/usr/bin/env bash
set -euo pipefail
APP_PACKAGE="com.example.myapp"
LOG_TAG="MyApp"
# Clear logs
adb logcat -c
# Monitor logs with color
adb logcat | grep --color=always -E "$APP_PACKAGE|$LOG_TAG|FATAL|ERROR"
Install and Monitor Script
Create debug-install.sh:
#!/usr/bin/env bash
set -euo pipefail
APK="app/build/outputs/apk/debug/app-debug.apk"
PACKAGE="com.example.myapp"
ACTIVITY=".MainActivity"
echo "Installing $APK..."
adb install -r "$APK"
echo "Starting app..."
adb shell am start -n "$PACKAGE/$ACTIVITY"
echo "Monitoring logs (Ctrl+C to stop)..."
adb logcat -c
adb logcat | grep "$PACKAGE"
Start Emulator and Wait
Create start-emulator.sh:
#!/usr/bin/env bash
set -euo pipefail
AVD_NAME="Pixel7"
echo "Starting emulator: $AVD_NAME"
emulator -avd "$AVD_NAME" -no-snapshot-save &
# Wait for device to boot
echo "Waiting for device to boot..."
adb wait-for-device
# Wait for boot animation to finish
while [ "$(adb shell getprop init.svc.bootanim | tr -d '\r')" = "running" ]; do
sleep 1
done
echo "Emulator ready!"
# Optional: Install app
if [ -f "app-debug.apk" ]; then
echo "Installing app..."
adb install -r app-debug.apk
fi
echo "Done!"
CI/CD Testing
GitHub Actions Example
.github/workflows/test.yml:
name: Android CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Test Go code
run: |
cd golib
go test -v ./...
- name: Build gomobile AAR
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
cd golib
gomobile bind -target=android -o mylib.aar .
cp mylib.aar ../app/libs/
- name: Run unit tests
run: ./gradlew test
- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
script: ./gradlew connectedCheck
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: app/build/reports/tests/
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
Next Steps
- Build and publish - Use the Build workflow for release builds
- Increase coverage - Aim for >80% test coverage
- Profile performance - Identify and fix bottlenecks
- Monitor crashes - Set up crash reporting (Firebase Crashlytics)