From 9f67d8ad95f93a8d6b66e3b179c42655ebdd5c09 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Sun, 24 Sep 2023 13:56:26 +0300 Subject: [PATCH] OpenDelta: Add support for stream flashing on AB devices Directly download and flash at the same time. Requires extra info in json to work. Will just show the user the flash button and the download one to preserve old functionality TBD: Adapt the json generating shell script in build to include missing info --- src/eu/chainfire/opendelta/ABUpdate.java | 83 ++++--- src/eu/chainfire/opendelta/MainActivity.java | 19 +- src/eu/chainfire/opendelta/State.java | 1 + src/eu/chainfire/opendelta/UpdateService.java | 221 +++++++++++------- 4 files changed, 205 insertions(+), 119 deletions(-) diff --git a/src/eu/chainfire/opendelta/ABUpdate.java b/src/eu/chainfire/opendelta/ABUpdate.java index 98c2212..1896b33 100644 --- a/src/eu/chainfire/opendelta/ABUpdate.java +++ b/src/eu/chainfire/opendelta/ABUpdate.java @@ -44,6 +44,7 @@ class ABUpdate { private static final String PAYLOAD_PROPERTIES_PATH = "payload_properties.txt"; private static final String PREFS_IS_INSTALLING_UPDATE = "prefs_is_installing_update"; private static final String PREFS_IS_SUSPENDED = "prefs_is_suspended"; + private static final String FILE_PREFIX = "file://"; private static final long WAKELOCK_TIMEOUT = 60 * 60 * 1000; /* 1 hour */ // non UpdateEngine errors @@ -61,14 +62,16 @@ class ABUpdate { private String mZipPath; private ProgressListener mProgressListener; private boolean mBound; + private boolean mIsStream; private final UpdateEngineCallback mUpdateEngineCallback = new UpdateEngineCallback() { @Override public void onStatusUpdate(int status, float percent) { Logger.d("onStatusUpdate = " + status + " " + percent + "%%"); // downloading stage: 0% - 30% + // when streaming: 0% - 50% int offset = 0; - int weight = 30; + int weight = mIsStream ? 50 : 30; switch (status) { case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT: @@ -81,13 +84,15 @@ class ABUpdate { return; case UpdateEngine.UpdateStatusConstants.VERIFYING: // verifying stage: 30% - 35% - offset = 30; + // when streaming: 50% - 55% + offset = mIsStream ? 50 : 30; weight = 5; break; case UpdateEngine.UpdateStatusConstants.FINALIZING: // finalizing stage: 35% - 100% - offset = 35; - weight = 65; + // when streaming: 55% - 100% + offset = mIsStream ? 55 : 35; + weight = mIsStream ? 45 : 65; break; } @@ -120,12 +125,18 @@ class ABUpdate { Logger.ex(ex); return ERROR_INVALID; } - mZipPath = zipPath; + return start(zipPath, null, 0, 0, listener); + } + + public int start(String url, String[] headerKeyValuePairs, + long offset, long size, ProgressListener listener) { + mIsStream = headerKeyValuePairs != null; + mZipPath = url; mProgressListener = listener; if (isInstallingUpdate(mUpdateService)) { return -1; } - final int installing = startUpdate(); + final int installing = startUpdate(headerKeyValuePairs, offset, size); setInstallingUpdate(installing < 0, mUpdateService); return installing; } @@ -218,31 +229,32 @@ class ABUpdate { return true; } - private int startUpdate() { + private int startUpdate(String[] headerKeyValuePairs, long offset, long size) { + Logger.d("startUpdate. mIsStream=" + mIsStream); File file = new File(mZipPath); - if (!file.exists()) { - Log.e(TAG, "The given update doesn't exist"); - return ERROR_NOT_FOUND; - } - - long offset; - String[] headerKeyValuePairs; - try (ZipFile zipFile = new ZipFile(file)) { - offset = getZipEntryOffset(zipFile, PAYLOAD_BIN_PATH); - ZipEntry payloadPropEntry = zipFile.getEntry(PAYLOAD_PROPERTIES_PATH); - try (InputStream is = zipFile.getInputStream(payloadPropEntry); - InputStreamReader isr = new InputStreamReader(is); - BufferedReader br = new BufferedReader(isr)) { - List lines = new ArrayList<>(); - for (String line; (line = br.readLine()) != null;) { - lines.add(line); - } - headerKeyValuePairs = new String[lines.size()]; - headerKeyValuePairs = lines.toArray(headerKeyValuePairs); + if (!mIsStream) { + if (!file.exists()) { + Log.e(TAG, "The given update doesn't exist"); + return ERROR_NOT_FOUND; + } + try (ZipFile zipFile = new ZipFile(file)) { + offset = getZipEntryOffset(zipFile, PAYLOAD_BIN_PATH); + ZipEntry payloadPropEntry = zipFile.getEntry(PAYLOAD_PROPERTIES_PATH); + try (InputStream is = zipFile.getInputStream(payloadPropEntry); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr)) { + List lines = new ArrayList<>(); + for (String line; (line = br.readLine()) != null;) { + lines.add(line); + } + headerKeyValuePairs = new String[lines.size()]; + headerKeyValuePairs = lines.toArray(headerKeyValuePairs); + } + Logger.d("payload offset=" + offset); + } catch (IOException | IllegalArgumentException e) { + Log.e(TAG, "Could not prepare " + file, e); + return ERROR_CORRUPTED; } - } catch (IOException | IllegalArgumentException e) { - Log.e(TAG, "Could not prepare " + file, e); - return ERROR_CORRUPTED; } try { @@ -260,8 +272,17 @@ class ABUpdate { } } if (!bindCallbacks()) return ERROR_NOT_READY; - String zipFileUri = "file://" + file.getAbsolutePath(); - mUpdateEngine.applyPayload(zipFileUri, offset, 0, headerKeyValuePairs); + String zipFileUri = mIsStream ? mZipPath : FILE_PREFIX + file.getAbsolutePath(); + + Logger.d("Applying payload with params:"); + Logger.d("URI: " + zipFileUri); + Logger.d("offset: " + offset); + Logger.d("size: " + size); + Logger.d("headerKeyValuePairs:"); + for (int i = 0; i < headerKeyValuePairs.length; i++) + Logger.d(headerKeyValuePairs[i]); + + mUpdateEngine.applyPayload(zipFileUri, offset, size, headerKeyValuePairs); return -1; } diff --git a/src/eu/chainfire/opendelta/MainActivity.java b/src/eu/chainfire/opendelta/MainActivity.java index 5579ccf..045e55b 100644 --- a/src/eu/chainfire/opendelta/MainActivity.java +++ b/src/eu/chainfire/opendelta/MainActivity.java @@ -447,12 +447,14 @@ public class MainActivity extends Activity { mPrefs.edit().putString(UpdateService.PREF_LATEST_FULL_NAME, null).commit(); break; case State.ACTION_AVAILABLE: + case State.ACTION_AVAILABLE_STREAM: final String latest = mPrefs.getString( UpdateService.PREF_LATEST_FULL_NAME, null); if (latest != null) { String latestBase = latest.substring(0, latest.lastIndexOf('.')); enableBuild = true; + enableFlash = mState.equals(State.ACTION_AVAILABLE_STREAM); enableChangelog = true; updateVersion = latestBase; title = getString(R.string.state_action_build_full); @@ -562,7 +564,7 @@ public class MainActivity extends Activity { mFileFlashButton.setEnabled(mPermOk && !disableCheck); mCheckBtn.setVisibility(hideCheck ? View.GONE : View.VISIBLE); mFlashBtn.setVisibility(enableFlash ? View.VISIBLE : View.GONE); - mBuildBtn.setVisibility(!enableBuild || enableFlash ? View.GONE : View.VISIBLE); + mBuildBtn.setVisibility(enableBuild ? View.VISIBLE : View.GONE); mRebootBtn.setVisibility(enableReboot ? View.VISIBLE : View.GONE); mFileFlashButton.setVisibility(hideCheck ? View.GONE : View.VISIBLE); @@ -615,10 +617,14 @@ public class MainActivity extends Activity { public void onButtonFlashNowClick(View v) { if (Config.isABDevice()) { + if (mState.equals(State.ACTION_AVAILABLE_STREAM)) { + streamStart.run(); + return; + } flashStart.run(); - } else { - flashRecoveryWarning.run(); + return; } + flashRecoveryWarning.run(); } public void onButtonStopClick(View v) { @@ -722,6 +728,13 @@ public class MainActivity extends Activity { startUpdateService(UpdateService.ACTION_FLASH); }; + private final Runnable streamStart = () -> { + mCheckBtn.setEnabled(false); + mFlashBtn.setEnabled(false); + mBuildBtn.setEnabled(false); + startUpdateService(UpdateService.ACTION_STREAM); + }; + private void requestPermissions() { mPermOk = true; if (!Environment.isExternalStorageManager()) { diff --git a/src/eu/chainfire/opendelta/State.java b/src/eu/chainfire/opendelta/State.java index 4acc912..53a3c18 100644 --- a/src/eu/chainfire/opendelta/State.java +++ b/src/eu/chainfire/opendelta/State.java @@ -39,6 +39,7 @@ public class State { public static final String ACTION_AB_PAUSED = "action_ab_paused"; public static final String ACTION_AB_FINISHED = "action_ab_finished"; public static final String ACTION_AVAILABLE = "action_available"; + public static final String ACTION_AVAILABLE_STREAM = "action_available_stream"; public static final String ACTION_FLASH_FILE_NO_SUM = "action_flash_file_no_sum"; public static final String ACTION_FLASH_FILE_INVALID_SUM = "action_flash_file_invalid_sum"; public static final String ACTION_FLASH_FILE_READY = "action_flash_file_ready"; diff --git a/src/eu/chainfire/opendelta/UpdateService.java b/src/eu/chainfire/opendelta/UpdateService.java index 51b28df..da0a313 100644 --- a/src/eu/chainfire/opendelta/UpdateService.java +++ b/src/eu/chainfire/opendelta/UpdateService.java @@ -63,8 +63,11 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; import org.json.JSONArray; import org.json.JSONException; @@ -104,6 +107,7 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi public static final String ACTION_DOWNLOAD_STOP = "eu.chainfire.opendelta.action.DOWNLOAD_STOP"; public static final String ACTION_DOWNLOAD_PAUSE = "eu.chainfire.opendelta.action.DOWNLOAD_PAUSE"; public static final String ACTION_FLASH = "eu.chainfire.opendelta.action.FLASH"; + public static final String ACTION_STREAM = "eu.chainfire.opendelta.action.STREAM"; public static final String ACTION_ALARM = "eu.chainfire.opendelta.action.ALARM"; public static final String ACTION_SCHEDULER = "eu.chainfire.opendelta.action.SCHEDULER"; public static final String EXTRA_ALARM_ID = "eu.chainfire.opendelta.extra.ALARM_ID"; @@ -118,6 +122,9 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi public static final int NOTIFICATION_UPDATE = 2; public static final int NOTIFICATION_ERROR = 3; + private static final String PAYLOAD_PROP_OFFSET = "offset="; + private static final String PAYLOAD_PROP_SIZE = "FILE_SIZE="; + public static final String PREF_READY_FILENAME_NAME = "ready_filename"; public static final String PREF_LATEST_CHANGELOG = "latest_changelog"; @@ -141,6 +148,7 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi public static final String PREF_AUTO_UPDATE_METERED_NETWORKS = "auto_update_metered_networks"; public static final String PREF_LATEST_FULL_NAME = "latest_full_name"; + public static final String PREF_LATEST_PAYLOAD_PROPS = "latest_payload_props"; public static final String PREF_DOWNLOAD_SIZE = "download_size_long"; public static final int PREF_AUTO_DOWNLOAD_DISABLED = 0; @@ -372,6 +380,9 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi else flashUpdate(); } break; + case ACTION_STREAM: + if (checkPermissions()) flashABUpdate(true); + break; case ACTION_FLASH_FILE: if (intent.hasExtra(EXTRA_FILENAME)) { String flashFilename = intent.getStringExtra(EXTRA_FILENAME); @@ -532,8 +543,10 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi } Logger.d("Assuming update available"); - mState.update(State.ACTION_AVAILABLE, mPrefs.getLong(PREF_LAST_CHECK_TIME_NAME, - PREF_LAST_CHECK_TIME_DEFAULT)); + final Set propSet = mPrefs.getStringSet(PREF_LATEST_PAYLOAD_PROPS, null); + final String state = (propSet != null && propSet.size() > 0) + ? State.ACTION_AVAILABLE_STREAM : State.ACTION_AVAILABLE; + mState.update(state, mPrefs.getLong(PREF_LAST_CHECK_TIME_NAME, PREF_LAST_CHECK_TIME_DEFAULT)); maybeNotify(notify, latestBuild, null); return; } @@ -706,75 +719,6 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi return false; } - private List getNewestBuild() { - Logger.d("Checking for latest build"); - - String url = mConfig.getUrlBaseJson(); - - String buildData = Download.asString(url); - if (buildData == null || buildData.length() == 0) { - mState.update(State.ERROR_DOWNLOAD, url, Download.ERROR_CODE_NEWEST_BUILD); - mNotificationManager.cancel(NOTIFICATION_BUSY); - return null; - } - JSONObject object; - try { - object = new JSONObject(buildData); - JSONArray updatesList = object.getJSONArray("response"); - String latestBuild = null; - String urlOverride = null; - String sumOverride = null; - for (int i = 0; i < updatesList.length(); i++) { - if (updatesList.isNull(i)) { - continue; - } - try { - JSONObject build = updatesList.getJSONObject(i); - String fileName = new File(build.getString("filename")).getName(); - String urlOvr = null; - String sumOvr = null; - if (build.has("url")) - urlOvr = build.getString("url"); - if (build.has("sha256url")) - sumOvr = build.getString("sha256url"); - Logger.d("parsed from json:"); - Logger.d("fileName= " + fileName); - if (isMatchingImage(fileName)) - latestBuild = fileName; - if (urlOvr != null && !urlOvr.equals("")) { - urlOverride = urlOvr; - Logger.d("url= " + urlOverride); - } - if (sumOvr != null && !sumOvr.equals("")) { - sumOverride = sumOvr; - Logger.d("sha256 url= " + sumOverride); - } - } catch (JSONException e) { - Logger.ex(e); - } - } - - List ret = new ArrayList<>(); - if (latestBuild != null) { - ret.add(latestBuild); - if (urlOverride != null) { - ret.add(urlOverride); - if (sumOverride != null) { - ret.add(sumOverride); - mIsUrlOverride = true; - mSumUrlOvr = sumOverride; - } - } - } - return ret; - - } catch (Exception e) { - Logger.ex(e); - } - mState.update(State.ERROR_UNOFFICIAL, mConfig.getVersion()); - return null; - } - public ProgressListener getSUMProgress(String state, String filename) { final long[] last = new long[] { 0, SystemClock.elapsedRealtime() }; final String _state = state; @@ -1067,10 +1011,16 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi } private void flashABUpdate() { - Logger.d("flashABUpdate"); + flashABUpdate(false); + } + + private void flashABUpdate(final boolean isStream) { + Logger.d("flashABUpdate. isStream=" + isStream); String flashFilename; try { - flashFilename = handleUpdateCleanup(); + flashFilename = isStream + ? mPrefs.getString(PREF_READY_FILENAME_NAME, null) + : handleUpdateCleanup(); } catch (Exception ex) { mIsUpdateRunning = false; mState.update(State.ERROR_AB_FLASH, ABUpdate.ERROR_NOT_FOUND); @@ -1084,12 +1034,41 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi // Clear the Download size to hide while flashing mPrefs.edit().putLong(PREF_DOWNLOAD_SIZE, -1).commit(); - final String _filename = new File(flashFilename).getName(); + String _filename = null; + if (isStream) { + final int eIndex = flashFilename.lastIndexOf('.'); + final int sIndex = flashFilename.lastIndexOf('/', eIndex); + _filename = flashFilename.substring(sIndex + 1, eIndex); + } else { + _filename = new File(flashFilename).getName(); + } mState.update(State.ACTION_AB_FLASH, 0f, 0L, 100L, _filename, null); newFlashNotification(_filename); - final int code = ABUpdate.getInstance(this).start(flashFilename, mProgressListener); + int code = -1; + if (isStream) { + Set payloadSet = mPrefs.getStringSet(PREF_LATEST_PAYLOAD_PROPS, null); + List payloadProps = new ArrayList<>(); + long offset = 0; + long size = 0; + for (String str : payloadSet) { + if (offset == 0 && str.startsWith(PAYLOAD_PROP_OFFSET)) { + offset = Long.parseLong(str.substring(PAYLOAD_PROP_OFFSET.length(), str.length())); + continue; + } + if (size == 0 && str.startsWith(PAYLOAD_PROP_SIZE)) + size = Long.parseLong(str.substring(PAYLOAD_PROP_SIZE.length(), str.length())); + payloadProps.add(str); + } + String[] headerKeyValuePairs = new String[payloadProps.size()]; + for (int i = 0; i < payloadProps.size(); i++) + headerKeyValuePairs[i] = payloadProps.get(i); + code = ABUpdate.getInstance(this).start(flashFilename, headerKeyValuePairs, + offset, size, mProgressListener); + } else { + code = ABUpdate.getInstance(this).start(flashFilename, mProgressListener); + } if (code < 0) { mLastProgressTime = new long[] { 0, SystemClock.elapsedRealtime() }; mProgressListener.setStatus(_filename); @@ -1284,6 +1263,7 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi private void clearState() { SharedPreferences.Editor editor = mPrefs.edit(); editor.putString(PREF_LATEST_FULL_NAME, null); + editor.putString(PREF_LATEST_PAYLOAD_PROPS, null); editor.putString(PREF_READY_FILENAME_NAME, null); editor.putString(PREF_LATEST_CHANGELOG, null); editor.putLong(PREF_DOWNLOAD_SIZE, -1); @@ -1329,26 +1309,88 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi (new File(mConfig.getPathBase())).mkdir(); (new File(mConfig.getPathFlashAfterUpdate())).mkdir(); - List latestBuildWithUrl = getNewestBuild(); - String latestBuild; + Logger.d("Checking for latest build"); + + String url = mConfig.getUrlBaseJson(); + String latestBuild = null; + String urlOverride = null; + String sumOverride = null; + List payloadProps = null; + + String buildData = Download.asString(url); + if (buildData == null || buildData.length() == 0) { + mState.update(State.ERROR_DOWNLOAD, url, Download.ERROR_CODE_NEWEST_BUILD); + mNotificationManager.cancel(NOTIFICATION_BUSY); + } + JSONObject object; + try { + object = new JSONObject(buildData); + JSONArray updatesList = object.getJSONArray("response"); + for (int i = 0; i < updatesList.length(); i++) { + if (updatesList.isNull(i)) { + continue; + } + try { + JSONObject build = updatesList.getJSONObject(i); + String fileName = new File(build.getString("filename")).getName(); + if (build.has("url")) + urlOverride = build.getString("url"); + if (build.has("sha256url")) + sumOverride = build.getString("sha256url"); + if (build.has("payload")) { + payloadProps = new ArrayList<>(); + JSONArray payloadList = build.getJSONArray("payload"); + for (int j = 0; j < payloadList.length(); j++) { + if (payloadList.isNull(j)) continue; + JSONObject prop = payloadList.getJSONObject(j); + Iterator keys = prop.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + payloadProps.add(key + "=" + prop.get(key)); + } + } + } + Logger.d("parsed from json:"); + Logger.d("fileName= " + fileName); + if (isMatchingImage(fileName)) + latestBuild = fileName; + if (urlOverride != null && !urlOverride.equals("")) + Logger.d("url= " + urlOverride); + if (sumOverride != null && !sumOverride.equals("")) { + Logger.d("sha256 url= " + sumOverride); + } + if (payloadProps != null) { + for (String str : payloadProps) { + Logger.d(str); + } + } + } catch (JSONException e) { + Logger.ex(e); + } + } + } catch (Exception e) { + Logger.ex(e); + mState.update(State.ERROR_UNOFFICIAL, mConfig.getVersion()); + return; + } + // if we don't even find a build on dl no sense to continue - if (latestBuildWithUrl == null || latestBuildWithUrl.size() == 0) { + if (latestBuild == null || latestBuild.length() == 0) { Logger.d("no latest build found at " + mConfig.getUrlBaseJson() + " for " + mConfig.getDevice()); return; } - latestBuild = latestBuildWithUrl.get(0); String latestFetch; String latestFetchSUM; - if (latestBuildWithUrl.size() < 3) { + if (urlOverride == null || sumOverride == null) { latestFetch = mConfig.getUrlBase() + latestBuild + mConfig.getUrlSuffix(); latestFetchSUM = mConfig.getUrlBaseSum() + latestBuild + ".sha256sum" + mConfig.getUrlSuffix(); } else { - latestFetch = latestBuildWithUrl.get(1); - latestFetchSUM = latestBuildWithUrl.get(2); + latestFetch = urlOverride; + latestFetchSUM = sumOverride; } Logger.d("latest build for device " + mConfig.getDevice() + " is " + latestFetch); @@ -1374,10 +1416,19 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi updateAvailable ? latestBuild : null).commit(); if (!updateAvailable) return; + if (payloadProps != null) { + mPrefs.edit().putStringSet(PREF_LATEST_PAYLOAD_PROPS, + payloadProps.stream().collect(Collectors.toSet())).commit(); + mPrefs.edit().putString(PREF_READY_FILENAME_NAME, latestFetch).commit(); + Logger.d("update supports streaming"); + } else { + mPrefs.edit().remove(PREF_LATEST_PAYLOAD_PROPS).commit(); + } + final String changelog = getChangelogString(); mPrefs.edit().putString(PREF_LATEST_CHANGELOG, changelog).commit(); - if (checkExistingBuild(latestBuildWithUrl, latestFetchSUM)) return; + if (checkExistingBuild(latestBuild, latestFetchSUM)) return; final long size = Download.getSize(latestFetch); mPrefs.edit().putLong(PREF_DOWNLOAD_SIZE, size).commit(); @@ -1437,8 +1488,8 @@ public class UpdateService extends Service implements OnSharedPreferenceChangeLi }); } - private boolean checkExistingBuild(List latestBuildWithUrl, String latestFetchSUM) { - String fn = mConfig.getPathBase() + latestBuildWithUrl.get(0); + private boolean checkExistingBuild(String latestBuild, String latestFetchSUM) { + String fn = mConfig.getPathBase() + latestBuild; File file = new File(fn); if (file.exists()) { if (checkBuildSHA256Sum(latestFetchSUM, fn)) {