App 2.18.0: install from a USB stick
Settings -> App -> "Install from USB stick" (car build only). It looks in the top folder of every mounted volume (found from the app's own folder on it, since the head unit has no file picker) for NilsMG4-unstable-<version>.apk, takes the newest one newer than the installed version, checks it against its .sha256 sidecar and the package name, and installs it with the same PackageInstaller session as the over-the-air update. The stable build gets a stub and no button. Tested on the emulator: 2.18.0 installed a 2.18.1 from the storage's top folder without a confirmation, as the system user, so the OTA install step should not need one either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -62,6 +62,17 @@ At start the app only checks, and shows "Update available" when there is one. It
|
|||||||
downloads or installs on its own, and the upload service never checks: installing restarts
|
downloads or installs on its own, and the upload service never checks: installing restarts
|
||||||
the app, which must not happen mid-drive on its own.
|
the app, which must not happen mid-drive on its own.
|
||||||
|
|
||||||
|
## Installing from a USB stick
|
||||||
|
|
||||||
|
For development, without the forge: copy `NilsMG4-unstable-<version>.apk` **and** its `.sha256`
|
||||||
|
from `build/car/` to the top folder of a USB stick, plug it in, and press **Install from USB
|
||||||
|
stick** on the Settings tab. The app takes the newest such file that is newer than the installed
|
||||||
|
version, checks it against the `.sha256`, checks the package, and installs it with the same
|
||||||
|
installer session as an over-the-air update. The car build only; the stable build has no button.
|
||||||
|
|
||||||
|
On the Android 9 emulator (car build, system user) this installed without asking, from the
|
||||||
|
storage's top folder; on the car the stick's top folder is used the same way.
|
||||||
|
|
||||||
## When it says something else
|
## When it says something else
|
||||||
|
|
||||||
| Message | Meaning |
|
| Message | Meaning |
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
|||||||
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
|
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
|
||||||
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
|
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
|
||||||
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
|
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
|
||||||
|
- 2026-10-18 — App 2.18.0: Settings → App → "Install from USB stick" (car build only): the newest
|
||||||
|
`NilsMG4-unstable-<version>.apk` newer than the installed one in a stick's top folder, checked
|
||||||
|
against its `.sha256` and the package, installed with the OTA installer session. Tested on the
|
||||||
|
emulator: 2.18.0 installed a 2.18.1 from the storage's top folder silently, as the system user —
|
||||||
|
so the OTA install step should not need a confirmation either.
|
||||||
- 2026-10-18 — App 2.17.5: Server tab fits the car's window. The "Your private connection" card is
|
- 2026-10-18 — App 2.17.5: Server tab fits the car's window. The "Your private connection" card is
|
||||||
gone (its text was out of date); Import from file sits with Save and Test in one row; the
|
gone (its text was out of date); Import from file sits with Save and Test in one row; the
|
||||||
installation ID is shown as text, not edited, since it is derived from the head unit (a config
|
installation ID is shown as text, not edited, since it is derived from the head unit (a config
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 94
|
versionCode 95
|
||||||
versionName "2.17.5"
|
versionName "2.18.0"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
.setText(getString(R.string.settings_version, BuildConfig.VERSION_NAME));
|
.setText(getString(R.string.settings_version, BuildConfig.VERSION_NAME));
|
||||||
settingsPane.findViewById(R.id.settings_about_button).setOnClickListener(v -> showAbout());
|
settingsPane.findViewById(R.id.settings_about_button).setOnClickListener(v -> showAbout());
|
||||||
settingsPane.findViewById(R.id.settings_update_button).setOnClickListener(v -> checkForUpdate(v));
|
settingsPane.findViewById(R.id.settings_update_button).setOnClickListener(v -> checkForUpdate(v));
|
||||||
|
View usbButton = settingsPane.findViewById(R.id.settings_usb_button);
|
||||||
|
usbButton.setOnClickListener(v -> installFromUsb(v));
|
||||||
|
// The stable build installs nothing itself, so it has no use for the button.
|
||||||
|
if (!UpdateHook.isSupported()) usbButton.setVisibility(View.GONE);
|
||||||
|
|
||||||
// Unstable builds check for a newer pre-release; the stable flavor's UpdateHook is
|
// Unstable builds check for a newer pre-release; the stable flavor's UpdateHook is
|
||||||
// a no-op and does not even contain the updater.
|
// a no-op and does not even contain the updater.
|
||||||
@@ -408,13 +412,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* SHA-256 verified; the final result remains visible until dismissed.
|
* SHA-256 verified; the final result remains visible until dismissed.
|
||||||
*/
|
*/
|
||||||
private void checkForUpdate(View button) {
|
private void checkForUpdate(View button) {
|
||||||
|
runUpdate(button, R.string.update_checking, listener -> UpdateHook.checkInBackground(this, listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Installs the newest car build from the top folder of a USB stick (unstable builds). */
|
||||||
|
private void installFromUsb(View button) {
|
||||||
|
runUpdate(button, R.string.update_usb_looking, listener -> UpdateHook.installFromUsbInBackground(this, listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The update dialog: progress while [start] runs, then its result. */
|
||||||
|
private void runUpdate(View button, int startText,
|
||||||
|
java.util.function.Consumer<UpdateHook.Listener> start) {
|
||||||
button.setEnabled(false);
|
button.setEnabled(false);
|
||||||
android.widget.LinearLayout panel = new android.widget.LinearLayout(this);
|
android.widget.LinearLayout panel = new android.widget.LinearLayout(this);
|
||||||
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
|
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||||
int padding = (int) (24 * getResources().getDisplayMetrics().density);
|
int padding = (int) (24 * getResources().getDisplayMetrics().density);
|
||||||
panel.setPadding(padding, padding, padding, padding);
|
panel.setPadding(padding, padding, padding, padding);
|
||||||
TextView status = new TextView(this);
|
TextView status = new TextView(this);
|
||||||
status.setText(R.string.update_checking);
|
status.setText(startText);
|
||||||
status.setTextAppearance(R.style.Text_EV_Body);
|
status.setTextAppearance(R.style.Text_EV_Body);
|
||||||
android.widget.ProgressBar progress = new android.widget.ProgressBar(this, null,
|
android.widget.ProgressBar progress = new android.widget.ProgressBar(this, null,
|
||||||
android.R.attr.progressBarStyleHorizontal);
|
android.R.attr.progressBarStyleHorizontal);
|
||||||
@@ -432,7 +447,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.create();
|
.create();
|
||||||
showKeepingFullScreen(dialog);
|
showKeepingFullScreen(dialog);
|
||||||
UpdateHook.checkInBackground(this, new UpdateHook.Listener() {
|
start.accept(new UpdateHook.Listener() {
|
||||||
@Override public void onProgress(int percent) {
|
@Override public void onProgress(int percent) {
|
||||||
if (percent < 0) {
|
if (percent < 0) {
|
||||||
progress.setIndeterminate(true);
|
progress.setIndeterminate(true);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_app_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_app_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||||
<TextView android:id="@+id/settings_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
<TextView android:id="@+id/settings_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/settings_update_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/btn_check_update" app:icon="@drawable/ic_refresh" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_update_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/btn_check_update" app:icon="@drawable/ic_refresh" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_usb_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_usb_update" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/settings_about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_about" app:icon="@drawable/ic_info" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_about" app:icon="@drawable/ic_info" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_app_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_app_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||||
<TextView android:id="@+id/settings_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
<TextView android:id="@+id/settings_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/settings_update_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/btn_check_update" app:icon="@drawable/ic_refresh" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_update_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/btn_check_update" app:icon="@drawable/ic_refresh" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_usb_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_usb_update" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/settings_about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_about" app:icon="@drawable/ic_info" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/settings_about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/btn_about" app:icon="@drawable/ic_info" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -182,6 +182,8 @@
|
|||||||
<string name="ota_install_failed">Installation failed: %1$s. The APK is in Downloads.</string>
|
<string name="ota_install_failed">Installation failed: %1$s. The APK is in Downloads.</string>
|
||||||
<string name="update_check">Check for updates</string>
|
<string name="update_check">Check for updates</string>
|
||||||
<string name="update_checking">Checking for a new OTA version…</string>
|
<string name="update_checking">Checking for a new OTA version…</string>
|
||||||
|
<string name="update_usb_looking">Looking on the USB stick…</string>
|
||||||
|
<string name="btn_usb_update">Install from USB stick</string>
|
||||||
<string name="update_dialog_title">Software update</string>
|
<string name="update_dialog_title">Software update</string>
|
||||||
<string name="btn_check_update">Check for updates</string>
|
<string name="btn_check_update">Check for updates</string>
|
||||||
<string name="btn_about">About Nils\' MG4</string>
|
<string name="btn_about">About Nils\' MG4</string>
|
||||||
|
|||||||
@@ -30,5 +30,10 @@ final class UpdateHook {
|
|||||||
if (listener != null) listener.onResult("This build has no automatic updates");
|
if (listener != null) listener.onResult("This build has no automatic updates");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Answers immediately that this channel does not install updates. */
|
||||||
|
static void installFromUsbInBackground(Context context, Listener listener) {
|
||||||
|
if (listener != null) listener.onResult("This build does not install updates");
|
||||||
|
}
|
||||||
|
|
||||||
static boolean isSupported() { return false; }
|
static boolean isSupported() { return false; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/** Finding a car build on a USB stick — pure JVM. */
|
||||||
|
public class UsbUpdateTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theStickRootIsFourLevelsAboveTheAppFolder() {
|
||||||
|
assertEquals(new File("/storage/1234-ABCD"), UsbUpdate.volumeRoot(
|
||||||
|
new File("/storage/1234-ABCD/Android/data/com.stinnesbeck.nilsmg4.unstable/files")));
|
||||||
|
assertEquals(new File("/storage/emulated/0"), UsbUpdate.volumeRoot(
|
||||||
|
new File("/storage/emulated/0/Android/data/com.stinnesbeck.nilsmg4.unstable/files")));
|
||||||
|
assertNull(UsbUpdate.volumeRoot(new File("/data/user/0/com.stinnesbeck.nilsmg4.unstable/files")));
|
||||||
|
assertNull(UsbUpdate.volumeRoot(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theNewestNewerCarBuildIsPicked() {
|
||||||
|
String[] names = {"NilsMG4-unstable-2.17.4.apk", "NilsMG4-unstable-2.18.0.apk", "NilsMG4-unstable-2.18.0.apk.sha256",
|
||||||
|
"NilsMG4-unstable-2.17.10.apk", "NilsMG4-stable-9.0.0.apk", "FatihsMG4-unstable-9.9.9.apk", "notes.txt"};
|
||||||
|
assertEquals("NilsMG4-unstable-2.18.0.apk", UsbUpdate.pickNewest(names, "2.17.5-unstable"));
|
||||||
|
assertEquals("NilsMG4-unstable-2.17.10.apk", UsbUpdate.pickNewest(
|
||||||
|
new String[]{"NilsMG4-unstable-2.17.4.apk", "NilsMG4-unstable-2.17.10.apk"}, "2.17.5-unstable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nothingNewerMeansNothing() {
|
||||||
|
assertNull(UsbUpdate.pickNewest(new String[]{"NilsMG4-unstable-2.17.5.apk"}, "2.17.5-unstable"));
|
||||||
|
assertNull(UsbUpdate.pickNewest(new String[0], "2.17.5-unstable"));
|
||||||
|
assertNull(UsbUpdate.pickNewest(null, "2.17.5-unstable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,81 @@ final class UpdateHook {
|
|||||||
}, "ota-check").start();
|
}, "ota-check").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the newest car build from the top folder of a USB stick (UsbUpdate), with the
|
||||||
|
* same checks as the over-the-air update except the server. Background thread; [listener]
|
||||||
|
* is called on the main thread.
|
||||||
|
*/
|
||||||
|
static void installFromUsbInBackground(Context context, Listener listener) {
|
||||||
|
Context app = context.getApplicationContext();
|
||||||
|
android.os.Handler main = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
|
if (!checking.compareAndSet(false, true)) {
|
||||||
|
if (listener != null) main.post(() -> listener.onResult("Update check already in progress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new Thread(() -> {
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
message = runUsbInstall(app);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.w(TAG, "USB install threw", t);
|
||||||
|
message = "Installing from USB failed";
|
||||||
|
} finally {
|
||||||
|
checking.set(false);
|
||||||
|
}
|
||||||
|
if (listener != null) {
|
||||||
|
final String result = message;
|
||||||
|
main.post(() -> listener.onResult(result));
|
||||||
|
}
|
||||||
|
}, "usb-install").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String runUsbInstall(Context app) throws Exception {
|
||||||
|
String current = app.getPackageManager().getPackageInfo(app.getPackageName(), 0).versionName;
|
||||||
|
File best = null;
|
||||||
|
String bestName = null;
|
||||||
|
for (File dir : app.getExternalFilesDirs(null)) {
|
||||||
|
File root = UsbUpdate.volumeRoot(dir);
|
||||||
|
if (root == null) continue;
|
||||||
|
String[] names = root.list();
|
||||||
|
String name = UsbUpdate.pickNewest(names, bestName == null ? current
|
||||||
|
: OtaUpdater.versionFromAssetName(bestName));
|
||||||
|
if (name != null) {
|
||||||
|
best = new File(root, name);
|
||||||
|
bestName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best == null) {
|
||||||
|
return "No newer NilsMG4-unstable-<version>.apk on a USB stick (installed: " + current + ")";
|
||||||
|
}
|
||||||
|
File sidecar = new File(best.getPath() + ".sha256");
|
||||||
|
String expected = sidecar.isFile() && sidecar.length() < 16 * 1024
|
||||||
|
? OtaUpdater.parseExpectedSha256(new String(java.nio.file.Files.readAllBytes(sidecar.toPath()),
|
||||||
|
java.nio.charset.StandardCharsets.UTF_8), best.getName())
|
||||||
|
: null;
|
||||||
|
if (expected == null) return "Not installed: " + best.getName() + ".sha256 is missing or unreadable";
|
||||||
|
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||||
|
try (java.io.InputStream in = new java.io.FileInputStream(best)) {
|
||||||
|
byte[] buffer = new byte[64 * 1024];
|
||||||
|
int count;
|
||||||
|
while ((count = in.read(buffer)) != -1) digest.update(buffer, 0, count);
|
||||||
|
}
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (byte b : digest.digest()) hex.append(String.format(java.util.Locale.US, "%02x", b));
|
||||||
|
if (!hex.toString().equalsIgnoreCase(expected)) return "Not installed: " + best.getName() + " does not match its SHA-256";
|
||||||
|
String version = OtaUpdater.versionFromAssetName(best.getName());
|
||||||
|
switch (OtaUpdater.install(app, best)) {
|
||||||
|
case SESSION_STARTED:
|
||||||
|
return "Installing " + version + " from USB… the app restarts when it is done";
|
||||||
|
case INSTALL_UI_STARTED:
|
||||||
|
return "Confirm the installation of " + version + " on the screen";
|
||||||
|
case UNREADABLE_ARCHIVE:
|
||||||
|
return "Not installed: " + best.getName() + " is not this app";
|
||||||
|
default:
|
||||||
|
return "Not installed: the installer could not start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** The blocking part. Returns a line describing what happened, for the UI. */
|
/** The blocking part. Returns a line describing what happened, for the UI. */
|
||||||
private static String runCheck(Context app, Listener listener,
|
private static String runCheck(Context app, Listener listener,
|
||||||
android.os.Handler main) throws IOException {
|
android.os.Handler main) throws IOException {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installing a car build from a USB stick — UNSTABLE BUILDS ONLY. For development: copy
|
||||||
|
* {@code NilsMG4-unstable-<version>.apk} and its {@code .sha256} (both from build-car) to the top
|
||||||
|
* folder of a stick, plug it in, press the button on the Settings tab.
|
||||||
|
*
|
||||||
|
* The same checks as the over-the-air update apply, minus the server: the newest such file that
|
||||||
|
* is newer than the installed version, a matching SHA-256 sidecar, this app's package, and the
|
||||||
|
* installer's own signature check. The pure parts are here, unit-tested; UpdateHook does the IO.
|
||||||
|
*/
|
||||||
|
final class UsbUpdate {
|
||||||
|
|
||||||
|
private UsbUpdate() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top folder of the volume an app-specific folder lives on:
|
||||||
|
* /storage/1234-ABCD/Android/data/<package>/files → /storage/1234-ABCD. Null when the path
|
||||||
|
* does not have that shape. This is how a stick is found without a file picker, which the
|
||||||
|
* head unit does not have.
|
||||||
|
*/
|
||||||
|
static File volumeRoot(File appFilesDir) {
|
||||||
|
if (appFilesDir == null) return null;
|
||||||
|
File data = appFilesDir.getParentFile() == null ? null : appFilesDir.getParentFile().getParentFile();
|
||||||
|
File android = data == null ? null : data.getParentFile();
|
||||||
|
if (!"files".equals(appFilesDir.getName()) || data == null || !"data".equals(data.getName())
|
||||||
|
|| android == null || !"Android".equals(android.getName())) return null;
|
||||||
|
return android.getParentFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The car-build APK to install among [names]: the highest version that is newer than
|
||||||
|
* [currentVersion], or null. Only names build-car writes count.
|
||||||
|
*/
|
||||||
|
static String pickNewest(String[] names, String currentVersion) {
|
||||||
|
String best = null, bestVersion = null;
|
||||||
|
if (names == null) return null;
|
||||||
|
for (String name : names) {
|
||||||
|
if (name == null || !name.toLowerCase(Locale.US).startsWith("nilsmg4-unstable-")) continue;
|
||||||
|
String version = OtaUpdater.versionFromAssetName(name);
|
||||||
|
if (version == null || !OtaUpdater.isNewer(version, currentVersion)) continue;
|
||||||
|
if (bestVersion == null || OtaUpdater.isNewer(version, bestVersion)) {
|
||||||
|
best = name;
|
||||||
|
bestVersion = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user