Skip to content

Commit 1c6b39b

Browse files
GenericJamclaude
andcommitted
feat(deploy): preserve app data on --native when signing key matches
adb_install_all ran an unconditional 'adb uninstall' before 'adb install', wiping MOB_DATA_DIR (on-device identity, screen stores) on every native deploy — even an in-place update signed with the same committed debug keystore. Try 'adb install -r' first (preserves data when signatures match) and only fall back to uninstall+install when the in-place update is genuinely rejected (INSTALL_FAILED_UPDATE_INCOMPATIBLE, INSTALL_FAILED_VERSION_DOWNGRADE, ...). Decision extracted to NativeBuild.needs_clean_reinstall?/2 and unit-tested. Verified empirically: on the same device, 'mix mob.deploy --native' wiped the seeded identity while a manual 'adb install -r' of the same keystore-signed APK preserved it (firstInstallTime unchanged) — confirming the uninstall, not a signature mismatch, was the cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 34c8d31 commit 1c6b39b

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ Full module documentation: [hexdocs.pm/mob_dev](https://hexdocs.pm/mob_dev).
88

99
---
1010

11+
## [Unreleased]
12+
13+
### Changed
14+
- **`mix mob.deploy --native` now preserves on-device app data when the signing
15+
key matches.** The Android install path previously ran an unconditional
16+
`adb uninstall` before `adb install`, which wiped `MOB_DATA_DIR` (on-device
17+
identity, screen stores) on **every** native deploy — even an in-place update
18+
signed with the same (e.g. committed) debug keystore. It now attempts
19+
`adb install -r` first and only falls back to uninstall + install when the
20+
in-place update is genuinely rejected (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`
21+
from a signature mismatch, `INSTALL_FAILED_VERSION_DOWNGRADE`, etc.). Apps that
22+
pin a committed debug keystore now keep their identity across `--native`
23+
redeploys. Decision logic extracted to `NativeBuild.needs_clean_reinstall?/2`
24+
and unit-tested.
25+
26+
---
27+
1128
## [0.6.12] - 2026-06-19
1229

1330
### Fixed

lib/mob_dev/native_build.ex

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,20 @@ defmodule MobDev.NativeBuild do
12321232
end)
12331233
end
12341234

1235+
@doc """
1236+
Decide whether an `adb install -r` result forces a clean (uninstall + install)
1237+
reinstall.
1238+
1239+
True when the in-place update was rejected — a non-zero exit or an
1240+
`INSTALL_FAILED_*` line (signature mismatch, version downgrade, etc.). A clean
1241+
reinstall wipes app data (on-device identity, screen stores), so the caller
1242+
only falls back to it when the in-place update genuinely cannot apply.
1243+
"""
1244+
@spec needs_clean_reinstall?(String.t(), integer()) :: boolean()
1245+
def needs_clean_reinstall?(install_output, exit_code) do
1246+
exit_code != 0 or String.contains?(install_output, "INSTALL_FAILED")
1247+
end
1248+
12351249
defp adb_install_all(apk, bundle_id, device_id) do
12361250
case System.cmd("adb", ["devices"], stderr_to_stdout: true) do
12371251
{output, 0} ->
@@ -1250,10 +1264,26 @@ defmodule MobDev.NativeBuild do
12501264
stderr_to_stdout: true
12511265
)
12521266

1253-
System.cmd("adb", ["-s", serial, "uninstall", bundle_id], stderr_to_stdout: true)
1267+
# Try an in-place reinstall first (`install -r`): it preserves app data
1268+
# (on-device identity, screen stores) when the signing key matches —
1269+
# the common case once an app pins a committed debug keystore. Only
1270+
# when the package can't be updated in place (signature mismatch,
1271+
# version downgrade) do we uninstall + install, which clears app data.
1272+
{first_out, first_rc} =
1273+
System.cmd("adb", ["-s", serial, "install", "-r", apk], stderr_to_stdout: true)
12541274

12551275
{install_out, install_rc} =
1256-
System.cmd("adb", ["-s", serial, "install", apk], stderr_to_stdout: true)
1276+
if needs_clean_reinstall?(first_out, first_rc) do
1277+
IO.puts(
1278+
" #{IO.ANSI.yellow()}In-place update rejected — reinstalling clean " <>
1279+
"(app data will be cleared)#{IO.ANSI.reset()}"
1280+
)
1281+
1282+
System.cmd("adb", ["-s", serial, "uninstall", bundle_id], stderr_to_stdout: true)
1283+
System.cmd("adb", ["-s", serial, "install", apk], stderr_to_stdout: true)
1284+
else
1285+
{first_out, first_rc}
1286+
end
12571287

12581288
if install_rc != 0 or String.contains?(install_out, "INSTALL_FAILED") do
12591289
reason =

test/mob_dev/native_build_test.exs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,4 +1742,24 @@ defmodule MobDev.NativeBuildTest do
17421742
refute NativeBuild.zig_required_message() =~ "—"
17431743
end
17441744
end
1745+
1746+
describe "needs_clean_reinstall?/2 (in-place install -r vs uninstall fallback)" do
1747+
test "false on a successful in-place reinstall — app data is preserved" do
1748+
refute NativeBuild.needs_clean_reinstall?("Success\n", 0)
1749+
end
1750+
1751+
test "true on signature mismatch (INSTALL_FAILED_UPDATE_INCOMPATIBLE)" do
1752+
out = "adb: failed to install app.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE]"
1753+
assert NativeBuild.needs_clean_reinstall?(out, 1)
1754+
end
1755+
1756+
test "true on version downgrade (INSTALL_FAILED_VERSION_DOWNGRADE)" do
1757+
out = "Failure [INSTALL_FAILED_VERSION_DOWNGRADE]"
1758+
assert NativeBuild.needs_clean_reinstall?(out, 1)
1759+
end
1760+
1761+
test "true on a non-zero exit even without an INSTALL_FAILED line" do
1762+
assert NativeBuild.needs_clean_reinstall?("error: device offline", 1)
1763+
end
1764+
end
17451765
end

0 commit comments

Comments
 (0)