Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobile-dev",
"version": "0.1.161",
"version": "0.1.162",
"description": "Keep a simulator beside the chat while building, running, or debugging iOS and Android apps. Stream devices, read native and Metro logs, analyze CPU and React profiles with TraceSift, render interactive CPU, memory, and FPS recordings, and control apps with Mobile Dev tools.",
"author": {
"name": "Callstack"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Track app CPU and memory, expand individual thread charts, and view device-wide
![](/img/screenshot_performance.png)

### App storage
See what an app has saved without adding code to it: MMKV, AsyncStorage, UserDefaults, SharedPreferences, DataStore, SQLite databases, and the largest files in its data directory. Open SQLite tables as a paged grid, search across every store at once, and open the Changes tab to watch keys being added, changed, and removed while you use the app, with only the changed part of JSON values highlighted. SDK data from Firebase, Google Analytics and similar libraries is grouped separately. Clear one store or reset all app data. Works on iOS simulators and debuggable Android builds; MMKV value types are guessed from the stored bytes, and encrypted MMKV instances show as locked.
See what an app has saved without adding code to it, whether it is built with React Native or natively: UserDefaults and App Group containers, Core Data and SwiftData, SharedPreferences, DataStore, MMKV, AsyncStorage, SQLite databases, and the largest files in its data directory. Open SQLite tables as a paged grid, open any file to see JSON, property lists, and archives decoded, search across every store at once, and open the Changes tab to watch keys being added, changed, and removed while you use the app, with only the changed part of JSON values highlighted. SDK data from Firebase, Google Analytics and similar libraries is grouped separately. Clear one store or reset all app data. Works on iOS simulators and debuggable Android builds; MMKV value types are guessed from the stored bytes, and encrypted MMKV instances show as locked.


## Requirements
Expand Down
8 changes: 4 additions & 4 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,19 @@ another; each caller keeps its own timing name (`ui.app_tools.icons`, `ui.storag
Results that finish after a surface or device change are not recorded. Recent URLs
and the last picked app stay in the panel's local storage and are never sent.

The Storage tool (`mobile_app_storage`, `mobile_app_storage_table`, `mobile_clear_app_storage`) uses the `storage`
The Storage tool (`mobile_app_storage`, `mobile_app_storage_table`, `mobile_app_storage_file`, `mobile_clear_app_storage`) uses the `storage`
surface. The server records `app_storage.operation.duration` in milliseconds and
`app_storage.operation.samples` per operation (`read`, `clear`, `reset`, `table`) with the device
`app_storage.operation.samples` per operation (`read`, `clear`, `reset`, `table`, `file`) with the device
platform and a success or failure outcome. The duration covers listing the data
directory, reading and decoding every recognized store, and for clears stopping the
app first. Unexpected failures report under `app_storage.<operation>` with a fixed
message; a release Android build, a missing app, or a stopped device counts only
through `server.error.outcome`. The panel records `ui.storage.list_apps`, `.icons`, `.read`, `.table`,
through `server.error.outcome`. The panel records `ui.storage.list_apps`, `.icons`, `.read`, `.table`, `.file`,
`.clear`, `.reset`, and `.launch` round trips, `ui.storage.process` for validating
a snapshot and computing the Changes feed, gauges `ui.storage.stores` and `ui.storage.entries`,
and counts `ui.storage.failure`. Bundle IDs, store names, paths, keys, values, and
file names are never sent; table names and cell values from `mobile_app_storage_table`
stay local too. Since 0.1.161 the Changes tab reads storage again every
and file contents from `mobile_app_storage_file` stay local too. Since 0.1.161 the Changes tab reads storage again every
5 seconds while it is open and the page is visible; each read is one `ui.storage.read`
sample, and the interval stops when the tab, panel, or page is hidden.

Expand Down
7 changes: 7 additions & 0 deletions examples/native-storage-android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.gradle/
.kotlin/
build/
app/build/
local.properties
*.iml
.idea/
48 changes: 48 additions & 0 deletions examples/native-storage-android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Native Storage Android test app

A small Kotlin app (plain `Activity`, no Compose, no Room or code generation) that
writes realistic data through the standard Android storage APIs. It is a fixture for
the plugin's on-device storage reader. Requires JDK 17, the Android SDK (API 36), and
an arm64 emulator or device running Android 8.0+.

## Run

Boot an emulator, then from the repository root:

```sh
./examples/native-storage-android/run.sh <device-serial>
```

The script builds the release variant with the Gradle wrapper, installs it with
`adb -s <serial> install -r`, and launches it. The release build type is
**debuggable** and signed with the debug keystore so the plugin can read app data
with `adb shell run-as`. It packages only `arm64-v8a` native libraries (MMKV).

- Package: `dev.mobile.nativestorage.android`
- Label: Native Storage
- Versions: AGP 8.13.2, Kotlin 2.2.21, Gradle 9.4.1

## What each button writes

| Button | Location | Data |
| --- | --- | --- |
| Sign in | `shared_prefs/dev.mobile.nativestorage.android_preferences.xml` | `user.name`, `user.id`, `isLoggedIn`, `lastLogin`, `fontScale`, `interests` (string set), `session` (JSON string) |
| Sign out | same file | Removes the keys above |
| Save secret | `shared_prefs/secret_prefs.xml` | `EncryptedSharedPreferences` (AES256_GCM master key): `api_token`, `pin` |
| Add note | `databases/notes.db` | One row per tap in `notes(id, title, body, pinned, created_at, thumbnail BLOB)` |
| Toggle theme | `files/datastore/settings.preferences_pb` | Preferences DataStore `settings`: `theme`, `notifications`, `launchCount` |
| Save profile | `files/datastore/profile.pb` | Proto DataStore with a hand-written protobuf wire-format serializer (name=1, age=2, premium=3, tags=4, address=5 with city=1, zip=2) |
| Write MMKV | `files/mmkv/mmkv.default`, `files/mmkv/cache` | Default instance (`user.name`, `launchCount`, `onboarded`) and the `cache` instance |
| Write files | `files/notes.txt`, `files/profile.json`, `cache/image-cache.bin` | Text, pretty JSON, 256 KB of random bytes |
| Clear everything | all of the above | Clears or deletes the data from inside the app |

A summary under the buttons shows what the app reads back after each tap.

## Inspect

```sh
adb -s <serial> shell run-as dev.mobile.nativestorage.android ls -R
```

Build output stays in the ignored `.gradle/`, `build/`, and `app/build/` directories.
This app is a local test fixture, excluded from plugin packaging.
48 changes: 48 additions & 0 deletions examples/native-storage-android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

android {
namespace = "dev.mobile.nativestorage.android"
compileSdk = 36

defaultConfig {
applicationId = "dev.mobile.nativestorage.android"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
// MMKV ships native libs for every ABI; keep the fixture small.
ndk { abiFilters += listOf("arm64-v8a") }
}

buildTypes {
release {
// The plugin reads app data through `adb shell run-as`, which needs a debuggable build.
isDebuggable = true
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("androidx.datastore:datastore-core:1.1.7")
implementation("com.google.protobuf:protobuf-javalite:4.28.3")
implementation("com.tencent:mmkv:1.3.14")
}
19 changes: 19 additions & 0 deletions examples/native-storage-android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name=".NativeStorageApp"
android:allowBackup="false"
android:icon="@android:drawable/sym_def_app_icon"
android:label="Native Storage"
android:theme="@android:style/Theme.DeviceDefault.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Loading
Loading