diff options
28 files changed, 747 insertions, 294 deletions
diff --git a/Android.mk b/Android.mk index 214d028da..fef5846ac 100644 --- a/Android.mk +++ b/Android.mk @@ -97,12 +97,48 @@ endif include $(BUILD_STATIC_LIBRARY) +librecovery_static_libraries := \ + $(TARGET_RECOVERY_UI_LIB) \ + libbootloader_message \ + libfusesideload \ + libminadbd \ + librecovery_ui \ + libminui \ + libverifier \ + libotautil \ + libasyncio \ + libbatterymonitor \ + libcrypto_utils \ + libcrypto \ + libext4_utils \ + libfs_mgr \ + libpng \ + libsparse \ + libvintf_recovery \ + libvintf \ + libhidl-gen-utils \ + libtinyxml2 \ + libziparchive \ + libbase \ + libutils \ + libcutils \ + liblog \ + libselinux \ + libz \ + # librecovery (static library) # =============================== include $(CLEAR_VARS) LOCAL_SRC_FILES := \ - install.cpp + adb_install.cpp \ + fuse_sdcard_provider.cpp \ + install.cpp \ + recovery.cpp \ + roots.cpp \ + +LOCAL_C_INCLUDES := \ + system/vold \ LOCAL_CFLAGS := $(recovery_common_cflags) @@ -113,13 +149,7 @@ endif LOCAL_MODULE := librecovery LOCAL_STATIC_LIBRARIES := \ - libminui \ - libotautil \ - libvintf_recovery \ - libcrypto_utils \ - libcrypto \ - libbase \ - libziparchive \ + $(librecovery_static_libraries) include $(BUILD_STATIC_LIBRARY) @@ -128,12 +158,8 @@ include $(BUILD_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_SRC_FILES := \ - adb_install.cpp \ - fuse_sdcard_provider.cpp \ logging.cpp \ - recovery.cpp \ recovery_main.cpp \ - roots.cpp \ LOCAL_MODULE := recovery @@ -147,38 +173,9 @@ LOCAL_USE_CLANG_LLD := false LOCAL_CFLAGS := $(recovery_common_cflags) -LOCAL_C_INCLUDES += \ - system/vold \ - LOCAL_STATIC_LIBRARIES := \ librecovery \ - $(TARGET_RECOVERY_UI_LIB) \ - libbootloader_message \ - libfusesideload \ - libminadbd \ - librecovery_ui \ - libminui \ - libverifier \ - libotautil \ - libasyncio \ - libbatterymonitor \ - libcrypto_utils \ - libcrypto \ - libext4_utils \ - libfs_mgr \ - libpng \ - libsparse \ - libvintf_recovery \ - libvintf \ - libhidl-gen-utils \ - libtinyxml2 \ - libziparchive \ - libbase \ - libcutils \ - libutils \ - liblog \ - libselinux \ - libz \ + $(librecovery_static_libraries) LOCAL_HAL_STATIC_LIBRARIES := libhealthd diff --git a/recovery.cpp b/recovery.cpp index b1a2900fd..56b2567d1 100644 --- a/recovery.cpp +++ b/recovery.cpp @@ -1049,6 +1049,7 @@ Device::BuiltinAction start_recovery(Device* device, const std::vector<std::stri continue; } } + optind = 1; printf("stage is [%s]\n", stage.c_str()); printf("reason is [%s]\n", reason); @@ -1062,6 +1063,11 @@ Device::BuiltinAction start_recovery(Device* device, const std::vector<std::stri ui->SetStage(st_cur, st_max); } + std::vector<std::string> title_lines = + android::base::Split(android::base::GetProperty("ro.bootimage.build.fingerprint", ""), ":"); + title_lines.insert(std::begin(title_lines), "Android Recovery"); + ui->SetTitle(title_lines); + device->StartRecovery(); printf("Command:"); diff --git a/recovery_main.cpp b/recovery_main.cpp index e21c782d0..5e82c6c1a 100644 --- a/recovery_main.cpp +++ b/recovery_main.cpp @@ -317,6 +317,7 @@ int main(int argc, char** argv) { } } } + optind = 1; if (locale.empty()) { if (has_cache) { diff --git a/screen_ui.cpp b/screen_ui.cpp index fd7a1bea5..f1b38781a 100644 --- a/screen_ui.cpp +++ b/screen_ui.cpp @@ -496,6 +496,10 @@ int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, return offset; } +void ScreenRecoveryUI::SetTitle(const std::vector<std::string>& lines) { + title_lines_ = lines; +} + // Redraws everything on the screen. Does not flip pages. Should only be called with updateMutex // locked. void ScreenRecoveryUI::draw_screen_locked() { @@ -529,11 +533,9 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked( int x = kMarginWidth + kMenuIndent; SetColor(INFO); - y += DrawTextLine(x, y, "Android Recovery", true); - std::string recovery_fingerprint = - android::base::GetProperty("ro.bootimage.build.fingerprint", ""); - for (const auto& chunk : android::base::Split(recovery_fingerprint, ":")) { - y += DrawTextLine(x, y, chunk, false); + + for (size_t i = 0; i < title_lines_.size(); i++) { + y += DrawTextLine(x, y, title_lines_[i], i == 0); } y += DrawTextLines(x, y, help_message); diff --git a/screen_ui.h b/screen_ui.h index 2d6b621d5..c90a2cd17 100644 --- a/screen_ui.h +++ b/screen_ui.h @@ -141,6 +141,7 @@ class ScreenRecoveryUI : public RecoveryUI { size_t ShowMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items, size_t initial_selection, bool menu_only, const std::function<int(int, bool)>& key_handler) override; + void SetTitle(const std::vector<std::string>& lines) override; void KeyLongPress(int) override; @@ -266,6 +267,8 @@ class ScreenRecoveryUI : public RecoveryUI { bool show_text; bool show_text_ever; // has show_text ever been true? + std::vector<std::string> title_lines_; + bool scrollable_menu_; std::unique_ptr<Menu> menu_; @@ -67,6 +67,8 @@ class StubRecoveryUI : public RecoveryUI { const std::function<int(int, bool)>& /* key_handler */) override { return initial_selection; } + + void SetTitle(const std::vector<std::string>& /* lines */) override {} }; #endif // RECOVERY_STUB_UI_H diff --git a/tests/Android.mk b/tests/Android.mk index cdc5b523a..853ca273b 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -174,8 +174,8 @@ librecovery_static_libraries := \ libtinyxml2 \ libziparchive \ libbase \ - libcutils \ libutils \ + libcutils \ liblog \ libselinux \ libz \ @@ -134,6 +134,8 @@ class RecoveryUI { // --- menu display --- + virtual void SetTitle(const std::vector<std::string>& lines) = 0; + // Displays a menu with the given 'headers' and 'items'. The supplied 'key_handler' callback, // which is typically bound to Device::HandleMenuKey(), should return the expected action for the // given key code and menu visibility (e.g. to move the cursor or to select an item). Caller sets diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp index 236644e7f..4a70b98a1 100644 --- a/updater/blockimg.cpp +++ b/updater/blockimg.cpp @@ -82,7 +82,7 @@ static void DeleteLastCommandFile() { // Parse the last command index of the last update and save the result to |last_command_index|. // Return true if we successfully read the index. -static bool ParseLastCommandFile(int* last_command_index) { +static bool ParseLastCommandFile(size_t* last_command_index) { const std::string& last_command_file = Paths::Get().last_command_file(); android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(last_command_file.c_str(), O_RDONLY))); if (fd == -1) { @@ -133,7 +133,7 @@ static bool FsyncDir(const std::string& dirname) { } // Update the last executed command index in the last_command_file. -static bool UpdateLastCommandIndex(int command_index, const std::string& command_string) { +static bool UpdateLastCommandIndex(size_t command_index, const std::string& command_string) { const std::string& last_command_file = Paths::Get().last_command_file(); std::string last_command_tmp = last_command_file + ".tmp"; std::string content = std::to_string(command_index) + "\n" + command_string; @@ -546,7 +546,6 @@ static int WriteBlocks(const RangeSet& tgt, const std::vector<uint8_t>& buffer, struct CommandParameters { std::vector<std::string> tokens; size_t cpos; - int cmdindex; const char* cmdname; const char* cmdline; std::string freestash; @@ -1666,7 +1665,6 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, return StringValue("t"); } - size_t start = 2; if (lines.size() < 4) { ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]", lines.size()); @@ -1691,8 +1689,8 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, params.createdstash = res; - // When performing an update, save the index and cmdline of the current command into - // the last_command_file. + // When performing an update, save the index and cmdline of the current command into the + // last_command_file. // Upon resuming an update, read the saved index first; then // 1. In verification mode, check if the 'move' or 'diff' commands before the saved index has // the expected target blocks already. If not, these commands cannot be skipped and we need @@ -1701,15 +1699,14 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, // 2. In update mode, skip all commands before the saved index. Therefore, we can avoid deleting // stashes with duplicate id unintentionally (b/69858743); and also speed up the update. // If an update succeeds or is unresumable, delete the last_command_file. - int saved_last_command_index; + bool skip_executed_command = true; + size_t saved_last_command_index; if (!ParseLastCommandFile(&saved_last_command_index)) { DeleteLastCommandFile(); - // We failed to parse the last command, set it explicitly to -1. - saved_last_command_index = -1; + // We failed to parse the last command. Disallow skipping executed commands. + skip_executed_command = false; } - start += 2; - // Build a map of the available commands std::unordered_map<std::string, const Command*> cmd_map; for (size_t i = 0; i < cmdcount; ++i) { @@ -1722,18 +1719,15 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, int rc = -1; + static constexpr size_t kTransferListHeaderLines = 4; // Subsequent lines are all individual transfer commands - for (size_t i = start; i < lines.size(); i++) { + for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) { const std::string& line = lines[i]; if (line.empty()) continue; + size_t cmdindex = i - kTransferListHeaderLines; params.tokens = android::base::Split(line, " "); params.cpos = 0; - if (i - start > std::numeric_limits<int>::max()) { - params.cmdindex = -1; - } else { - params.cmdindex = i - start; - } params.cmdname = params.tokens[params.cpos++].c_str(); params.cmdline = line.c_str(); params.target_verified = false; @@ -1756,9 +1750,9 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, // Skip all commands before the saved last command index when resuming an update, except for // "new" command. Because new commands read in the data sequentially. - if (params.canwrite && params.cmdindex != -1 && params.cmdindex <= saved_last_command_index && + if (params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index && cmdname != "new") { - LOG(INFO) << "Skipping already executed command: " << params.cmdindex + LOG(INFO) << "Skipping already executed command: " << cmdindex << ", last executed command for previous update: " << saved_last_command_index; continue; } @@ -1768,17 +1762,16 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, goto pbiudone; } - // In verify mode, check if the commands before the saved last_command_index have been - // executed correctly. If some target blocks have unexpected contents, delete the last command - // file so that we will resume the update from the first command in the transfer list. - if (!params.canwrite && saved_last_command_index != -1 && params.cmdindex != -1 && - params.cmdindex <= saved_last_command_index) { + // In verify mode, check if the commands before the saved last_command_index have been executed + // correctly. If some target blocks have unexpected contents, delete the last command file so + // that we will resume the update from the first command in the transfer list. + if (!params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index) { // TODO(xunchang) check that the cmdline of the saved index is correct. if ((cmdname == "move" || cmdname == "bsdiff" || cmdname == "imgdiff") && !params.target_verified) { LOG(WARNING) << "Previously executed command " << saved_last_command_index << ": " << params.cmdline << " doesn't produce expected target blocks."; - saved_last_command_index = -1; + skip_executed_command = false; DeleteLastCommandFile(); } } @@ -1789,7 +1782,7 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, goto pbiudone; } - if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) { + if (!UpdateLastCommandIndex(cmdindex, params.cmdline)) { LOG(WARNING) << "Failed to update the last command file."; } diff --git a/updater_sample/README.md b/updater_sample/README.md index 95e57dbe9..3f211ddba 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -44,6 +44,10 @@ saved uncompressed (`ZIP_STORED`), so that their data can be downloaded directly with the offset and length. As `payload.bin` itself is already in compressed format, the size penalty is marginal. +if `ab_config.force_switch_slot` set true device will boot to the +updated partition on next reboot; otherwise button "Switch Slot" will +become active, and user can manually set updated partition as the active slot. + Config files can be generated using `tools/gen_update_config.py`. Running `./tools/gen_update_config.py --help` shows usage of the script. @@ -85,10 +89,10 @@ which HTTP headers are supported. - [x] Add stop/reset the update - [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload` - [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules) -- [ ] Add tests for `MainActivity` -- [ ] Change partition demo +- [x] Deferred switch slot demo +- [ ] Add demo for passing NETWORK_ID to `UpdateEngine#applyPayload` - [ ] Verify system partition checksum for package -- [ ] Add non-A/B updates demo +- [?] Add non-A/B updates demo ## Running tests diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml index 7a12d3474..d9e56b4b3 100644 --- a/updater_sample/res/layout/activity_main.xml +++ b/updater_sample/res/layout/activity_main.xml @@ -178,6 +178,23 @@ android:text="Reset" /> </LinearLayout> + <TextView + android:id="@+id/textViewUpdateInfo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="14dp" + android:textColor="#777" + android:textSize="10sp" + android:textStyle="italic" + android:text="@string/finish_update_info" /> + + <Button + android:id="@+id/buttonSwitchSlot" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:onClick="onSwitchSlotClick" + android:text="@string/switch_slot" /> + </LinearLayout> </ScrollView> diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json index 46fbfa33e..f188c233b 100644 --- a/updater_sample/res/raw/sample.json +++ b/updater_sample/res/raw/sample.json @@ -20,5 +20,10 @@ } ], "authorization": "Basic my-secret-token" + }, + "ab_config": { + "__": "A/B (seamless) update configurations", + "__force_switch_slot": "if set true device will boot to a new slot, otherwise user manually switches slot on the screen", + "force_switch_slot": false } } diff --git a/updater_sample/res/values/strings.xml b/updater_sample/res/values/strings.xml index 2b671ee5d..db4a5dc67 100644 --- a/updater_sample/res/values/strings.xml +++ b/updater_sample/res/values/strings.xml @@ -18,4 +18,6 @@ <string name="action_reload">Reload</string> <string name="unknown">Unknown</string> <string name="close">CLOSE</string> + <string name="switch_slot">Switch Slot</string> + <string name="finish_update_info">To finish the update press the button below</string> </resources> diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java index 9bdd8b9e8..1e0fadc27 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java @@ -71,7 +71,7 @@ public class UpdateConfig implements Parcelable { JSONObject meta = o.getJSONObject("ab_streaming_metadata"); JSONArray propertyFilesJson = meta.getJSONArray("property_files"); PackageFile[] propertyFiles = - new PackageFile[propertyFilesJson.length()]; + new PackageFile[propertyFilesJson.length()]; for (int i = 0; i < propertyFilesJson.length(); i++) { JSONObject p = propertyFilesJson.getJSONObject(i); propertyFiles[i] = new PackageFile( @@ -87,6 +87,12 @@ public class UpdateConfig implements Parcelable { propertyFiles, authorization); } + + // TODO: parse only for A/B updates when non-A/B is implemented + JSONObject ab = o.getJSONObject("ab_config"); + boolean forceSwitchSlot = ab.getBoolean("force_switch_slot"); + c.mAbConfig = new AbConfig(forceSwitchSlot); + c.mRawJson = json; return c; } @@ -109,6 +115,9 @@ public class UpdateConfig implements Parcelable { /** metadata is required only for streaming update */ private StreamingMetadata mAbStreamingMetadata; + /** A/B update configurations */ + private AbConfig mAbConfig; + private String mRawJson; protected UpdateConfig() { @@ -119,6 +128,7 @@ public class UpdateConfig implements Parcelable { this.mUrl = in.readString(); this.mAbInstallType = in.readInt(); this.mAbStreamingMetadata = (StreamingMetadata) in.readSerializable(); + this.mAbConfig = (AbConfig) in.readSerializable(); this.mRawJson = in.readString(); } @@ -148,6 +158,10 @@ public class UpdateConfig implements Parcelable { return mAbStreamingMetadata; } + public AbConfig getAbConfig() { + return mAbConfig; + } + /** * @return File object for given url */ @@ -172,6 +186,7 @@ public class UpdateConfig implements Parcelable { dest.writeString(mUrl); dest.writeInt(mAbInstallType); dest.writeSerializable(mAbStreamingMetadata); + dest.writeSerializable(mAbConfig); dest.writeString(mRawJson); } @@ -185,9 +200,11 @@ public class UpdateConfig implements Parcelable { /** defines beginning of update data in archive */ private PackageFile[] mPropertyFiles; - /** SystemUpdaterSample receives the authorization token from the OTA server, in addition + /** + * SystemUpdaterSample receives the authorization token from the OTA server, in addition * to the package URL. It passes on the info to update_engine, so that the latter can - * fetch the data from the package server directly with the token. */ + * fetch the data from the package server directly with the token. + */ private String mAuthorization; public StreamingMetadata(PackageFile[] propertyFiles, String authorization) { @@ -239,4 +256,27 @@ public class UpdateConfig implements Parcelable { } } + /** + * A/B (seamless) update configurations. + */ + public static class AbConfig implements Serializable { + + private static final long serialVersionUID = 31044L; + + /** + * if set true device will boot to new slot, otherwise user manually + * switches slot on the screen. + */ + private boolean mForceSwitchSlot; + + public AbConfig(boolean forceSwitchSlot) { + this.mForceSwitchSlot = forceSwitchSlot; + } + + public boolean getForceSwitchSlot() { + return mForceSwitchSlot; + } + + } + } diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java new file mode 100644 index 000000000..9f0a04e33 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.systemupdatersample; + +import android.content.Context; +import android.os.UpdateEngine; +import android.os.UpdateEngineCallback; +import android.util.Log; + +import com.example.android.systemupdatersample.services.PrepareStreamingService; +import com.example.android.systemupdatersample.util.PayloadSpecs; +import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; +import com.example.android.systemupdatersample.util.UpdateEngineProperties; +import com.google.common.util.concurrent.AtomicDouble; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.DoubleConsumer; +import java.util.function.IntConsumer; + +/** + * Manages the update flow. Asynchronously interacts with the {@link UpdateEngine}. + */ +public class UpdateManager { + + private static final String TAG = "UpdateManager"; + + /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ + private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; + + private final UpdateEngine mUpdateEngine; + private final PayloadSpecs mPayloadSpecs; + + private AtomicInteger mUpdateEngineStatus = + new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); + private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN); + private AtomicDouble mProgress = new AtomicDouble(0); + + private final UpdateManager.UpdateEngineCallbackImpl + mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); + + private PayloadSpec mLastPayloadSpec; + private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); + + private IntConsumer mOnEngineStatusUpdateCallback = null; + private DoubleConsumer mOnProgressUpdateCallback = null; + private IntConsumer mOnEngineCompleteCallback = null; + + private final Object mLock = new Object(); + + public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) { + this.mUpdateEngine = updateEngine; + this.mPayloadSpecs = payloadSpecs; + } + + /** + * Binds to {@link UpdateEngine}. + */ + public void bind() { + this.mUpdateEngine.bind(mUpdateEngineCallback); + } + + /** + * Unbinds from {@link UpdateEngine}. + */ + public void unbind() { + this.mUpdateEngine.unbind(); + } + + /** + * @return a number from {@code 0.0} to {@code 1.0}. + */ + public float getProgress() { + return (float) this.mProgress.get(); + } + + /** + * Returns true if manual switching slot is required. Value depends on + * the update config {@code ab_config.force_switch_slot}. + */ + public boolean manualSwitchSlotRequired() { + return mManualSwitchSlotRequired.get(); + } + + /** + * Sets update engine status update callback. Value of {@code status} will + * be one of the values from {@link UpdateEngine.UpdateStatusConstants}. + * + * @param onStatusUpdateCallback a callback with parameter {@code status}. + */ + public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) { + synchronized (mLock) { + this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback; + } + } + + private Optional<IntConsumer> getOnEngineStatusUpdateCallback() { + synchronized (mLock) { + return mOnEngineStatusUpdateCallback == null + ? Optional.empty() + : Optional.of(mOnEngineStatusUpdateCallback); + } + } + + /** + * Sets update engine payload application complete callback. Value of {@code errorCode} will + * be one of the values from {@link UpdateEngine.ErrorCodeConstants}. + * + * @param onComplete a callback with parameter {@code errorCode}. + */ + public void setOnEngineCompleteCallback(IntConsumer onComplete) { + synchronized (mLock) { + this.mOnEngineCompleteCallback = onComplete; + } + } + + private Optional<IntConsumer> getOnEngineCompleteCallback() { + synchronized (mLock) { + return mOnEngineCompleteCallback == null + ? Optional.empty() + : Optional.of(mOnEngineCompleteCallback); + } + } + + /** + * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}. + * + * @param onProgressCallback a callback with parameter {@code progress}. + */ + public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) { + synchronized (mLock) { + this.mOnProgressUpdateCallback = onProgressCallback; + } + } + + private Optional<DoubleConsumer> getOnProgressUpdateCallback() { + synchronized (mLock) { + return mOnProgressUpdateCallback == null + ? Optional.empty() + : Optional.of(mOnProgressUpdateCallback); + } + } + + /** + * Requests update engine to stop any ongoing update. If an update has been applied, + * leave it as is. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void cancelRunningUpdate() { + try { + mUpdateEngine.cancel(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); + } + } + + /** + * Resets update engine to IDLE state. If an update has been applied it reverts it. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void resetUpdate() { + try { + mUpdateEngine.resetStatus(); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to reset the update", e); + } + } + + /** + * Applies the given update. + * + * <p>UpdateEngine works asynchronously. This method doesn't wait until + * end of the update.</p> + */ + public void applyUpdate(Context context, UpdateConfig config) { + mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN); + + if (!config.getAbConfig().getForceSwitchSlot()) { + mManualSwitchSlotRequired.set(true); + } else { + mManualSwitchSlotRequired.set(false); + } + + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { + applyAbNonStreamingUpdate(config); + } else { + applyAbStreamingUpdate(context, config); + } + } + + private void applyAbNonStreamingUpdate(UpdateConfig config) { + List<String> extraProperties = prepareExtraProperties(config); + + PayloadSpec payload; + try { + payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } catch (IOException e) { + Log.e(TAG, "Error creating payload spec", e); + return; + } + updateEngineApplyPayload(payload, extraProperties); + } + + private void applyAbStreamingUpdate(Context context, UpdateConfig config) { + List<String> extraProperties = prepareExtraProperties(config); + + Log.d(TAG, "Starting PrepareStreamingService"); + PrepareStreamingService.startService(context, config, (code, payloadSpec) -> { + if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { + extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); + config.getStreamingMetadata() + .getAuthorization() + .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); + updateEngineApplyPayload(payloadSpec, extraProperties); + } else { + Log.e(TAG, "PrepareStreamingService failed, result code is " + code); + } + }); + } + + private List<String> prepareExtraProperties(UpdateConfig config) { + List<String> extraProperties = new ArrayList<>(); + + if (!config.getAbConfig().getForceSwitchSlot()) { + // Disable switch slot on reboot, which is enabled by default. + // User will enable it manually by clicking "Switch Slot" button on the screen. + extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT); + } + return extraProperties; + } + + /** + * Applies given payload. + * + * <p>UpdateEngine works asynchronously. This method doesn't wait until + * end of the update.</p> + * + * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid + * payload properties (which come from OTA packages), or failing to set up the network + * with the given id.</p> + * + * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME} + * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload} + */ + private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) { + mLastPayloadSpec = payloadSpec; + + ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties()); + if (extraProperties != null) { + properties.addAll(extraProperties); + } + try { + mUpdateEngine.applyPayload( + payloadSpec.getUrl(), + payloadSpec.getOffset(), + payloadSpec.getSize(), + properties.toArray(new String[0])); + } catch (Exception e) { + Log.e(TAG, "UpdateEngine failed to apply the update", e); + } + } + + /** + * Sets the new slot that has the updated partitions as the active slot, + * which device will boot into next time. + * This method is only supposed to be called after the payload is applied. + * + * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size + * and payload metadata headers doesn't trigger new update. It can be used to just switch + * active A/B slot. + * + * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will + * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}. + */ + public void setSwitchSlotOnReboot() { + Log.d(TAG, "setSwitchSlotOnReboot invoked"); + List<String> extraProperties = new ArrayList<>(); + // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks. + extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL); + // It sets property SWITCH_SLOT_ON_REBOOT=1 by default. + // HTTP headers are not required, UpdateEngine is not expected to stream payload. + updateEngineApplyPayload(mLastPayloadSpec, extraProperties); + } + + private void onStatusUpdate(int status, float progress) { + int previousStatus = mUpdateEngineStatus.get(); + mUpdateEngineStatus.set(status); + mProgress.set(progress); + + getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress)); + + if (previousStatus != status) { + getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status)); + } + } + + private void onPayloadApplicationComplete(int errorCode) { + Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode); + mEngineErrorCode.set(errorCode); + + getOnEngineCompleteCallback() + .ifPresent(callback -> callback.accept(errorCode)); + } + + /** + * Helper class to delegate {@code update_engine} callbacks to UpdateManager + */ + class UpdateEngineCallbackImpl extends UpdateEngineCallback { + @Override + public void onStatusUpdate(int status, float percent) { + UpdateManager.this.onStatusUpdate(status, percent); + } + + @Override + public void onPayloadApplicationComplete(int errorCode) { + UpdateManager.this.onPayloadApplicationComplete(errorCode); + } + } + +} diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java index 222bb0a58..ac6e223e3 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java +++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java @@ -116,6 +116,8 @@ public class PrepareStreamingService extends IntentService { PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME ); + private final PayloadSpecs mPayloadSpecs = new PayloadSpecs(); + @Override protected void onHandleIntent(Intent intent) { Log.d(TAG, "On handle intent is called"); @@ -137,7 +139,7 @@ public class PrepareStreamingService extends IntentService { * 3. Checks OTA package compatibility with the device. * 4. Constructs {@link PayloadSpec} for streaming update. */ - private static PayloadSpec execute(UpdateConfig config) + private PayloadSpec execute(UpdateConfig config) throws IOException, PreparationFailedException { downloadPreStreamingFiles(config, OTA_PACKAGE_DIR); @@ -164,7 +166,7 @@ public class PrepareStreamingService extends IntentService { } } - return PayloadSpecs.forStreaming(config.getUrl(), + return mPayloadSpecs.forStreaming(config.getUrl(), payloadBinary.get().getOffset(), payloadBinary.get().getSize(), Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile()); @@ -176,7 +178,7 @@ public class PrepareStreamingService extends IntentService { * in directory {@code dir}. * @throws IOException when can't download a file */ - private static void downloadPreStreamingFiles(UpdateConfig config, String dir) + private void downloadPreStreamingFiles(UpdateConfig config, String dir) throws IOException { Log.d(TAG, "Deleting existing files from " + dir); for (String file : PRE_STREAMING_FILES_SET) { @@ -200,7 +202,7 @@ public class PrepareStreamingService extends IntentService { * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME} * @return true if OTA package is compatible with this device */ - private static boolean verifyPackageCompatibility(File file) { + private boolean verifyPackageCompatibility(File file) { try { return RecoverySystem.verifyPackageCompatibility(file); } catch (IOException e) { diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java index 170825635..9237bc794 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -18,10 +18,10 @@ package com.example.android.systemupdatersample.ui; import android.app.Activity; import android.app.AlertDialog; +import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.UpdateEngine; -import android.os.UpdateEngineCallback; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; @@ -31,19 +31,15 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import com.example.android.systemupdatersample.PayloadSpec; import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.UpdateConfig; -import com.example.android.systemupdatersample.services.PrepareStreamingService; +import com.example.android.systemupdatersample.UpdateManager; import com.example.android.systemupdatersample.util.PayloadSpecs; import com.example.android.systemupdatersample.util.UpdateConfigs; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineStatuses; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; /** * UI for SystemUpdaterSample app. @@ -52,10 +48,6 @@ public class MainActivity extends Activity { private static final String TAG = "MainActivity"; - /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ - private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; - private TextView mTextViewBuild; private Spinner mSpinnerConfigs; private TextView mTextViewConfigsDirHint; @@ -66,17 +58,13 @@ public class MainActivity extends Activity { private ProgressBar mProgressBar; private TextView mTextViewStatus; private TextView mTextViewCompletion; + private TextView mTextViewUpdateInfo; + private Button mButtonSwitchSlot; private List<UpdateConfig> mConfigs; - private AtomicInteger mUpdateEngineStatus = - new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); - - /** - * Listen to {@code update_engine} events. - */ - private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl(); - private final UpdateEngine mUpdateEngine = new UpdateEngine(); + private final UpdateManager mUpdateManager = + new UpdateManager(new UpdateEngine(), new PayloadSpecs()); @Override protected void onCreate(Bundle savedInstanceState) { @@ -93,21 +81,39 @@ public class MainActivity extends Activity { this.mProgressBar = findViewById(R.id.progressBar); this.mTextViewStatus = findViewById(R.id.textViewStatus); this.mTextViewCompletion = findViewById(R.id.textViewCompletion); + this.mTextViewUpdateInfo = findViewById(R.id.textViewUpdateInfo); + this.mButtonSwitchSlot = findViewById(R.id.buttonSwitchSlot); this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this)); uiReset(); loadUpdateConfigs(); - this.mUpdateEngine.bind(mUpdateEngineCallback); + this.mUpdateManager.setOnEngineStatusUpdateCallback(this::onStatusUpdate); + this.mUpdateManager.setOnProgressUpdateCallback(this::onProgressUpdate); + this.mUpdateManager.setOnEngineCompleteCallback(this::onPayloadApplicationComplete); } @Override protected void onDestroy() { - this.mUpdateEngine.unbind(); + this.mUpdateManager.setOnEngineStatusUpdateCallback(null); + this.mUpdateManager.setOnProgressUpdateCallback(null); + this.mUpdateManager.setOnEngineCompleteCallback(null); super.onDestroy(); } + @Override + protected void onResume() { + super.onResume(); + this.mUpdateManager.bind(); + } + + @Override + protected void onPause() { + this.mUpdateManager.unbind(); + super.onPause(); + } + /** * reload button is clicked */ @@ -137,7 +143,7 @@ public class MainActivity extends Activity { .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { uiSetUpdating(); - applyUpdate(getSelectedConfig()); + mUpdateManager.applyUpdate(this, getSelectedConfig()); }) .setNegativeButton(android.R.string.cancel, null) .show(); @@ -152,7 +158,7 @@ public class MainActivity extends Activity { .setMessage("Do you really want to cancel running update?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - stopRunningUpdate(); + mUpdateManager.cancelRunningUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); } @@ -167,36 +173,43 @@ public class MainActivity extends Activity { + " and restore old version?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - resetUpdate(); + mUpdateManager.resetUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); } /** + * switch slot button clicked + */ + public void onSwitchSlotClick(View view) { + mUpdateManager.setSwitchSlotOnReboot(); + } + + /** * Invoked when anything changes. The value of {@code status} will * be one of the values from {@link UpdateEngine.UpdateStatusConstants}, * and {@code percent} will be from {@code 0.0} to {@code 1.0}. */ - private void onStatusUpdate(int status, float percent) { - mProgressBar.setProgress((int) (100 * percent)); - if (mUpdateEngineStatus.get() != status) { - mUpdateEngineStatus.set(status); - runOnUiThread(() -> { - Log.e("UpdateEngine", "StatusUpdate - status=" - + UpdateEngineStatuses.getStatusText(status) - + "/" + status); - setUiStatus(status); - Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG) - .show(); - if (status != UpdateEngine.UpdateStatusConstants.IDLE) { - Log.d(TAG, "status changed, setting ui to updating mode"); - uiSetUpdating(); - } else { - Log.d(TAG, "status changed, resetting ui"); - uiReset(); - } - }); - } + private void onStatusUpdate(int status) { + runOnUiThread(() -> { + Log.e("UpdateEngine", "StatusUpdate - status=" + + UpdateEngineStatuses.getStatusText(status) + + "/" + status); + Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG) + .show(); + if (status == UpdateEngine.UpdateStatusConstants.IDLE) { + Log.d(TAG, "status changed, resetting ui"); + uiReset(); + } else { + Log.d(TAG, "status changed, setting ui to updating mode"); + uiSetUpdating(); + } + setUiStatus(status); + }); + } + + private void onProgressUpdate(double progress) { + mProgressBar.setProgress((int) (100 * progress)); } /** @@ -215,6 +228,13 @@ public class MainActivity extends Activity { + " " + state); Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show(); setUiCompletion(errorCode); + if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) { + // if update was successfully applied. + if (mUpdateManager.manualSwitchSlotRequired()) { + // Show "Switch Slot" button. + uiShowSwitchSlotInfo(); + } + } }); } @@ -231,6 +251,7 @@ public class MainActivity extends Activity { mProgressBar.setVisibility(ProgressBar.INVISIBLE); mTextViewStatus.setText(R.string.unknown); mTextViewCompletion.setText(R.string.unknown); + uiHideSwitchSlotInfo(); } /** sets ui updating mode */ @@ -245,6 +266,16 @@ public class MainActivity extends Activity { mProgressBar.setVisibility(ProgressBar.VISIBLE); } + private void uiShowSwitchSlotInfo() { + mButtonSwitchSlot.setEnabled(true); + mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777")); + } + + private void uiHideSwitchSlotInfo() { + mTextViewUpdateInfo.setTextColor(Color.parseColor("#AAAAAA")); + mButtonSwitchSlot.setEnabled(false); + } + /** * loads json configurations from configs dir that is defined in {@link UpdateConfigs}. */ @@ -286,108 +317,4 @@ public class MainActivity extends Activity { return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition()); } - /** - * Applies the given update - */ - private void applyUpdate(final UpdateConfig config) { - if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { - PayloadSpec payload; - try { - payload = PayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); - } catch (IOException e) { - Log.e(TAG, "Error creating payload spec", e); - Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG) - .show(); - return; - } - updateEngineApplyPayload(payload, null); - } else { - Log.d(TAG, "Starting PrepareStreamingService"); - PrepareStreamingService.startService(this, config, (code, payloadSpec) -> { - if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { - List<String> extraProperties = new ArrayList<>(); - extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); - config.getStreamingMetadata() - .getAuthorization() - .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); - updateEngineApplyPayload(payloadSpec, extraProperties); - } else { - Log.e(TAG, "PrepareStreamingService failed, result code is " + code); - Toast.makeText( - MainActivity.this, - "PrepareStreamingService failed, result code is " + code, - Toast.LENGTH_LONG).show(); - } - }); - } - } - - /** - * Applies given payload. - * - * UpdateEngine works asynchronously. This method doesn't wait until - * end of the update. - * - * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME} - * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload} - */ - private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) { - ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties()); - if (extraProperties != null) { - properties.addAll(extraProperties); - } - try { - mUpdateEngine.applyPayload( - payloadSpec.getUrl(), - payloadSpec.getOffset(), - payloadSpec.getSize(), - properties.toArray(new String[0])); - } catch (Exception e) { - Log.e(TAG, "UpdateEngine failed to apply the update", e); - Toast.makeText( - this, - "UpdateEngine failed to apply the update", - Toast.LENGTH_LONG).show(); - } - } - - /** - * Requests update engine to stop any ongoing update. If an update has been applied, - * leave it as is. - */ - private void stopRunningUpdate() { - try { - mUpdateEngine.cancel(); - } catch (Exception e) { - Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); - } - } - - /** - * Resets update engine to IDLE state. Requests to cancel any onging update, or to revert if an - * update has been applied. - */ - private void resetUpdate() { - try { - mUpdateEngine.resetStatus(); - } catch (Exception e) { - Log.w(TAG, "UpdateEngine failed to reset the update", e); - } - } - - /** - * Helper class to delegate {@code update_engine} callbacks to MainActivity - */ - class UpdateEngineCallbackImpl extends UpdateEngineCallback { - @Override - public void onStatusUpdate(int status, float percent) { - MainActivity.this.onStatusUpdate(status, percent); - } - - @Override - public void onPayloadApplicationComplete(int errorCode) { - MainActivity.this.onPayloadApplicationComplete(errorCode); - } - } - } diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java index 4db448a31..f06231726 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java @@ -32,7 +32,9 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** The helper class that creates {@link PayloadSpec}. */ -public final class PayloadSpecs { +public class PayloadSpecs { + + public PayloadSpecs() {} /** * The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package @@ -43,7 +45,7 @@ public final class PayloadSpecs { * zip file. So we enumerate the entries to identify the offset of the payload file. * http://developer.android.com/reference/java/util/zip/ZipFile.html#entries() */ - public static PayloadSpec forNonStreaming(File packageFile) throws IOException { + public PayloadSpec forNonStreaming(File packageFile) throws IOException { boolean payloadFound = false; long payloadOffset = 0; long payloadSize = 0; @@ -100,7 +102,7 @@ public final class PayloadSpecs { /** * Creates a {@link PayloadSpec} for streaming update. */ - public static PayloadSpec forStreaming(String updateUrl, + public PayloadSpec forStreaming(String updateUrl, long offset, long size, File propertiesFile) throws IOException { @@ -115,7 +117,7 @@ public final class PayloadSpecs { /** * Converts an {@link PayloadSpec} to a string. */ - public static String toString(PayloadSpec payloadSpec) { + public String specToString(PayloadSpec payloadSpec) { return "<PayloadSpec url=" + payloadSpec.getUrl() + ", offset=" + payloadSpec.getOffset() + ", size=" + payloadSpec.getSize() @@ -124,6 +126,4 @@ public final class PayloadSpecs { + ">"; } - private PayloadSpecs() {} - } diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java index 6d319c5af..f06ddf7fc 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java @@ -34,6 +34,7 @@ public final class UpdateEngineErrorCodes { * Error code from the update engine. Values must agree with the ones in * system/update_engine/common/error_code.h. */ + public static final int UNKNOWN = -1; public static final int UPDATED_BUT_NOT_ACTIVE = 52; private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>(); diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java new file mode 100644 index 000000000..e368f14d2 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.systemupdatersample.util; + +/** + * Utility class for properties that will be passed to {@code UpdateEngine#applyPayload}. + */ +public final class UpdateEngineProperties { + + /** + * The property indicating that the update engine should not switch slot + * when the device reboots. + */ + public static final String PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT = "SWITCH_SLOT_ON_REBOOT=0"; + + /** + * The property to skip post-installation. + * https://source.android.com/devices/tech/ota/ab/#post-installation + */ + public static final String PROPERTY_SKIP_POST_INSTALL = "RUN_POST_INSTALL=0"; + + private UpdateEngineProperties() {} +} diff --git a/updater_sample/tests/Android.mk b/updater_sample/tests/Android.mk index a1a4664dc..9aec372e3 100644 --- a/updater_sample/tests/Android.mk +++ b/updater_sample/tests/Android.mk @@ -23,9 +23,9 @@ LOCAL_MODULE_TAGS := tests LOCAL_JAVA_LIBRARIES := \ android.test.base.stubs \ android.test.runner.stubs \ - guava \ + guava +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test \ mockito-target-minus-junit4 -LOCAL_STATIC_JAVA_LIBRARIES := android-support-test LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample LOCAL_PROGUARD_ENABLED := disabled diff --git a/updater_sample/tests/res/raw/update_config_stream_001.json b/updater_sample/tests/res/raw/update_config_stream_001.json index 15127cf2c..be51b7c95 100644 --- a/updater_sample/tests/res/raw/update_config_stream_001.json +++ b/updater_sample/tests/res/raw/update_config_stream_001.json @@ -10,5 +10,8 @@ "size": 8 } ] + }, + "ab_config": { + "force_switch_slot": true } } diff --git a/updater_sample/tests/res/raw/update_config_stream_002.json b/updater_sample/tests/res/raw/update_config_stream_002.json index cf4469b1c..5d7874cdb 100644 --- a/updater_sample/tests/res/raw/update_config_stream_002.json +++ b/updater_sample/tests/res/raw/update_config_stream_002.json @@ -1,5 +1,8 @@ { "__": "*** Generated using tools/gen_update_config.py ***", + "ab_config": { + "force_switch_slot": false + }, "ab_install_type": "STREAMING", "ab_streaming_metadata": { "property_files": [ diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java index 0975e76be..000f5663b 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java @@ -18,6 +18,7 @@ package com.example.android.systemupdatersample; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import android.content.Context; import android.support.test.InstrumentationRegistry; @@ -45,7 +46,8 @@ public class UpdateConfigTest { private static final String JSON_NON_STREAMING = "{\"name\": \"vip update\", \"url\": \"file:///builds/a.zip\", " - + " \"ab_install_type\": \"NON_STREAMING\"}"; + + " \"ab_install_type\": \"NON_STREAMING\"," + + " \"ab_config\": { \"force_switch_slot\": false } }"; @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -82,6 +84,7 @@ public class UpdateConfigTest { config.getStreamingMetadata().getPropertyFiles()[0].getFilename()); assertEquals(195, config.getStreamingMetadata().getPropertyFiles()[0].getOffset()); assertEquals(8, config.getStreamingMetadata().getPropertyFiles()[0].getSize()); + assertTrue(config.getAbConfig().getForceSwitchSlot()); } @Test @@ -94,7 +97,8 @@ public class UpdateConfigTest { @Test public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception { String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\"," - + " \"ab_install_type\": \"NON_STREAMING\"}"; + + " \"ab_install_type\": \"NON_STREAMING\"," + + " \"ab_config\": { \"force_switch_slot\": false } }"; UpdateConfig config = UpdateConfig.fromJson(json); thrown.expect(RuntimeException.class); config.getUpdatePackageFile(); diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java new file mode 100644 index 000000000..0657a5eb6 --- /dev/null +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.systemupdatersample; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.UpdateEngine; +import android.os.UpdateEngineCallback; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.example.android.systemupdatersample.util.PayloadSpecs; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.function.IntConsumer; + +/** + * Tests for {@link UpdateManager} + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UpdateManagerTest { + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private UpdateEngine mUpdateEngine; + @Mock + private PayloadSpecs mPayloadSpecs; + private UpdateManager mUpdateManager; + + @Before + public void setUp() { + mUpdateManager = new UpdateManager(mUpdateEngine, mPayloadSpecs); + } + + @Test + public void storesProgressThenInvokesCallbacks() { + IntConsumer statusUpdateCallback = mock(IntConsumer.class); + + // When UpdateManager is bound to update_engine, it passes + // UpdateManager.UpdateEngineCallbackImpl as a callback to update_engine. + when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { + UpdateEngineCallback callback = answer.getArgument(0); + callback.onStatusUpdate(/*engineStatus*/ 4, /*engineProgress*/ 0.2f); + return null; + }); + + mUpdateManager.setOnEngineStatusUpdateCallback(statusUpdateCallback); + + // Making sure that manager.getProgress() returns correct progress + // in "onEngineStatusUpdate" callback. + doAnswer(answer -> { + assertEquals(0.2f, mUpdateManager.getProgress(), 1E-5); + return null; + }).when(statusUpdateCallback).accept(anyInt()); + + mUpdateManager.bind(); + + verify(statusUpdateCallback, times(1)).accept(4); + } + +} diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java deleted file mode 100644 index 01014168a..000000000 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.systemupdatersample.ui; - -import static org.junit.Assert.assertNotNull; - -import android.support.test.filters.MediumTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Make sure that the main launcher activity opens up properly, which will be - * verified by {@link #activityLaunches}. - */ -@RunWith(AndroidJUnit4.class) -@MediumTest -public class MainActivityTest { - - @Rule - public final ActivityTestRule<MainActivity> mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - /** - * Verifies that the activity under test can be launched. - */ - @Test - public void activityLaunches() { - assertNotNull(mActivityRule.getActivity()); - } -} diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java index d9e54652f..3ba84c116 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java @@ -55,6 +55,8 @@ public class PayloadSpecsTest { private Context mTargetContext; private Context mTestContext; + private PayloadSpecs mPayloadSpecs; + @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -64,6 +66,7 @@ public class PayloadSpecsTest { mTestContext = InstrumentationRegistry.getContext(); mTestDir = mTargetContext.getFilesDir(); + mPayloadSpecs = new PayloadSpecs(); } @Test @@ -75,7 +78,7 @@ public class PayloadSpecsTest { java.nio.file.Files.deleteIfExists(packageFile.toPath()); java.nio.file.Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package), packageFile.toPath()); - PayloadSpec spec = PayloadSpecs.forNonStreaming(packageFile); + PayloadSpec spec = mPayloadSpecs.forNonStreaming(packageFile); assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl()); assertEquals("correct payload offset", @@ -90,7 +93,7 @@ public class PayloadSpecsTest { @Test public void forNonStreaming_IOException() throws Exception { thrown.expect(IOException.class); - PayloadSpecs.forNonStreaming(new File("/fake/news.zip")); + mPayloadSpecs.forNonStreaming(new File("/fake/news.zip")); } @Test @@ -100,7 +103,7 @@ public class PayloadSpecsTest { long size = 200; File propertiesFile = createMockPropertiesFile(); - PayloadSpec spec = PayloadSpecs.forStreaming(url, offset, size, propertiesFile); + PayloadSpec spec = mPayloadSpecs.forStreaming(url, offset, size, propertiesFile); assertEquals("same url", url, spec.getUrl()); assertEquals("same offset", offset, spec.getOffset()); assertEquals("same size", size, spec.getSize()); diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py index 4efa9f1c4..7fb64f7fc 100755 --- a/updater_sample/tools/gen_update_config.py +++ b/updater_sample/tools/gen_update_config.py @@ -46,10 +46,11 @@ class GenUpdateConfig(object): AB_INSTALL_TYPE_STREAMING = 'STREAMING' AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING' - def __init__(self, package, url, ab_install_type): + def __init__(self, package, url, ab_install_type, ab_force_switch_slot): self.package = package self.url = url self.ab_install_type = ab_install_type + self.ab_force_switch_slot = ab_force_switch_slot self.streaming_required = ( # payload.bin and payload_properties.txt must exist. 'payload.bin', @@ -80,6 +81,9 @@ class GenUpdateConfig(object): 'url': self.url, 'ab_streaming_metadata': streaming_metadata, 'ab_install_type': self.ab_install_type, + 'ab_config': { + 'force_switch_slot': self.ab_force_switch_slot, + } } def _gen_ab_streaming_metadata(self): @@ -126,6 +130,11 @@ def main(): # pylint: disable=missing-docstring default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING, choices=ab_install_type_choices, help='A/B update installation type') + parser.add_argument('--ab_force_switch_slot', + type=bool, + default=False, + help='if set true device will boot to a new slot, otherwise user manually ' + 'switches slot on the screen') parser.add_argument('package', type=str, help='OTA package zip file') @@ -144,7 +153,8 @@ def main(): # pylint: disable=missing-docstring gen = GenUpdateConfig( package=args.package, url=args.url, - ab_install_type=args.ab_install_type) + ab_install_type=args.ab_install_type, + ab_force_switch_slot=args.ab_force_switch_slot) gen.run() gen.write(args.out) print('Config is written to ' + args.out) |