C++ class for device-specific code

Replace the device-specific functions with a class.  Move some of the
key handling (for log visibility toggling and rebooting) into the UI
class.  Fix up the key handling so there is less crosstalk between the
immediate keys and the queued keys (an increasing annoyance on
button-limited devices).

Change-Id: I8bdea6505da7974631bf3d9ac3ee308f8c0f76e1
diff --git a/Android.mk b/Android.mk
index 527aa1b..be9ff9e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -34,7 +34,7 @@
 LOCAL_MODULE_TAGS := eng
 
 ifeq ($(TARGET_RECOVERY_UI_LIB),)
-  LOCAL_SRC_FILES += default_recovery_ui.c
+  LOCAL_SRC_FILES += default_device.cpp
 else
   LOCAL_STATIC_LIBRARIES += $(TARGET_RECOVERY_UI_LIB)
 endif
diff --git a/default_device.cpp b/default_device.cpp
new file mode 100644
index 0000000..265ed07
--- /dev/null
+++ b/default_device.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <linux/input.h>
+
+#include "common.h"
+#include "device.h"
+#include "screen_ui.h"
+
+static const char* HEADERS[] = { "Volume up/down to move highlight;",
+                                 "enter button to select.",
+                                 "",
+                                 NULL };
+
+static const char* ITEMS[] =  {"reboot system now",
+                               "apply update from external storage",
+                               "apply update from cache",
+                               "wipe data/factory reset",
+                               "wipe cache partition",
+                               NULL };
+
+class DefaultUI : public ScreenRecoveryUI {
+  public:
+    virtual KeyAction CheckKey(int key) {
+        if (key == KEY_HOME) {
+            return TOGGLE;
+        }
+        return ENQUEUE;
+    }
+};
+
+class DefaultDevice : public Device {
+  public:
+    DefaultDevice() :
+        ui(new DefaultUI) {
+    }
+
+    RecoveryUI* GetUI() { return ui; }
+
+    int HandleMenuKey(int key, int visible) {
+        if (visible) {
+            switch (key) {
+              case KEY_DOWN:
+              case KEY_VOLUMEDOWN:
+                return kHighlightDown;
+
+              case KEY_UP:
+              case KEY_VOLUMEUP:
+                return kHighlightUp;
+
+              case KEY_ENTER:
+                return kInvokeItem;
+            }
+        }
+
+        return kNoAction;
+    }
+
+    BuiltinAction InvokeMenuItem(int menu_position) {
+        switch (menu_position) {
+          case 0: return REBOOT;
+          case 1: return APPLY_EXT;
+          case 2: return APPLY_CACHE;
+          case 3: return WIPE_DATA;
+          case 4: return WIPE_CACHE;
+          default: return NO_ACTION;
+        }
+    }
+
+    const char* const* GetMenuHeaders() { return HEADERS; }
+    const char* const* GetMenuItems() { return ITEMS; }
+
+  private:
+    RecoveryUI* ui;
+};
+
+Device* make_device() {
+    return new DefaultDevice();
+}
diff --git a/default_recovery_ui.c b/default_recovery_ui.c
deleted file mode 100644
index d56164e..0000000
--- a/default_recovery_ui.c
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2009 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.
- */
-
-#include <linux/input.h>
-
-#include "recovery_ui.h"
-#include "common.h"
-
-char* MENU_HEADERS[] = { "Android system recovery utility",
-                         "",
-                         NULL };
-
-char* MENU_ITEMS[] = { "reboot system now",
-                       "apply update from external storage",
-                       "wipe data/factory reset",
-                       "wipe cache partition",
-                       "apply update from cache",
-                       NULL };
-
-void device_ui_init(UIParameters* ui_parameters) {
-}
-
-int device_recovery_start() {
-    return 0;
-}
-
-int device_toggle_display(volatile char* key_pressed, int key_code) {
-    return key_code == KEY_HOME;
-}
-
-int device_reboot_now(volatile char* key_pressed, int key_code) {
-    return 0;
-}
-
-int device_handle_key(int key_code, int visible) {
-    if (visible) {
-        switch (key_code) {
-            case KEY_DOWN:
-            case KEY_VOLUMEDOWN:
-                return HIGHLIGHT_DOWN;
-
-            case KEY_UP:
-            case KEY_VOLUMEUP:
-                return HIGHLIGHT_UP;
-
-            case KEY_ENTER:
-                return SELECT_ITEM;
-        }
-    }
-
-    return NO_ACTION;
-}
-
-int device_perform_action(int which) {
-    return which;
-}
-
-int device_wipe_data() {
-    return 0;
-}
diff --git a/device.h b/device.h
new file mode 100644
index 0000000..8096a8d
--- /dev/null
+++ b/device.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#ifndef _RECOVERY_DEVICE_H
+#define _RECOVERY_DEVICE_H
+
+#include "ui.h"
+
+class Device {
+  public:
+    virtual ~Device() { }
+
+    // Called to obtain the UI object that should be used to display
+    // the recovery user interface for this device.  You should not
+    // have called Init() on the UI object already, the caller will do
+    // that after this method returns.
+    virtual RecoveryUI* GetUI() = 0;
+
+    // Called when recovery starts up (after the UI has been obtained
+    // and initialized and after the arguments have been parsed, but
+    // before anything else).
+    virtual void StartRecovery() { };
+
+    // enum KeyAction { NONE, TOGGLE, REBOOT };
+
+    // // Called in the input thread when a new key (key_code) is
+    // // pressed.  *key_pressed is an array of KEY_MAX+1 bytes
+    // // indicating which other keys are already pressed.  Return a
+    // // KeyAction to indicate action should be taken immediately.
+    // // These actions happen when recovery is not waiting for input
+    // // (eg, in the midst of installing a package).
+    // virtual KeyAction CheckImmediateKeyAction(volatile char* key_pressed, int key_code) = 0;
+
+    // Called from the main thread when recovery is at the main menu
+    // and waiting for input, and a key is pressed.  (Note that "at"
+    // the main menu does not necessarily mean the menu is visible;
+    // recovery will be at the main menu with it invisible after an
+    // unsuccessful operation [ie OTA package failure], or if recovery
+    // is started with no command.)
+    //
+    // key is the code of the key just pressed.  (You can call
+    // IsKeyPressed() on the RecoveryUI object you returned from GetUI
+    // if you want to find out if other keys are held down.)
+    //
+    // visible is true if the menu is visible.
+    //
+    // Return one of the defined constants below in order to:
+    //
+    //   - move the menu highlight (kHighlight{Up,Down})
+    //   - invoke the highlighted item (kInvokeItem)
+    //   - do nothing (kNoAction)
+    //   - invoke a specific action (a menu position: any non-negative number)
+    virtual int HandleMenuKey(int key, int visible) = 0;
+
+    enum BuiltinAction { NO_ACTION, REBOOT, APPLY_EXT, APPLY_CACHE,
+                         WIPE_DATA, WIPE_CACHE };
+
+    // Perform a recovery action selected from the menu.
+    // 'menu_position' will be the item number of the selected menu
+    // item, or a non-negative number returned from
+    // device_handle_key().  The menu will be hidden when this is
+    // called; implementations can call ui_print() to print
+    // information to the screen.  If the menu position is one of the
+    // builtin actions, you can just return the corresponding enum
+    // value.  If it is an action specific to your device, you
+    // actually perform it here and return NO_ACTION.
+    virtual BuiltinAction InvokeMenuItem(int menu_position) = 0;
+
+    static const int kNoAction = -1;
+    static const int kHighlightUp = -2;
+    static const int kHighlightDown = -3;
+    static const int kInvokeItem = -4;
+
+    // Called when we do a wipe data/factory reset operation (either via a
+    // reboot from the main system with the --wipe_data flag, or when the
+    // user boots into recovery manually and selects the option from the
+    // menu.)  Can perform whatever device-specific wiping actions are
+    // needed.  Return 0 on success.  The userdata and cache partitions
+    // are erased AFTER this returns (whether it returns success or not).
+    virtual int WipeData() { return 0; }
+
+    // Return the headers (an array of strings, one per line,
+    // NULL-terminated) for the main menu.  Typically these tell users
+    // what to push to move the selection and invoke the selected
+    // item.
+    virtual const char* const* GetMenuHeaders() = 0;
+
+    // Return the list of menu items (an array of strings,
+    // NULL-terminated).  The menu_position passed to InvokeMenuItem
+    // will correspond to the indexes into this array.
+    virtual const char* const* GetMenuItems() = 0;
+};
+
+// The device-specific library must define this function (or the
+// default one will be used, if there is no device-specific library).
+// It returns the Device object that recovery should use.
+Device* make_device();
+
+#endif  // _DEVICE_H
diff --git a/recovery.cpp b/recovery.cpp
index d1af3ac..d028cc9 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -37,9 +37,9 @@
 #include "minui/minui.h"
 #include "minzip/DirUtil.h"
 #include "roots.h"
-#include "recovery_ui.h"
 #include "ui.h"
 #include "screen_ui.h"
+#include "device.h"
 
 static const struct option OPTIONS[] = {
   { "send_intent", required_argument, NULL, 's' },
@@ -401,7 +401,7 @@
 }
 
 static const char**
-prepend_title(const char** headers) {
+prepend_title(const char* const* headers) {
     const char* title[] = { "Android system recovery <"
                             EXPAND(RECOVERY_API_VERSION) "e>",
                             "",
@@ -410,7 +410,7 @@
     // count the number of lines in our title, plus the
     // caller-provided headers.
     int count = 0;
-    const char** p;
+    const char* const* p;
     for (p = title; *p; ++p, ++count);
     for (p = headers; *p; ++p, ++count);
 
@@ -425,7 +425,7 @@
 
 static int
 get_menu_selection(const char* const * headers, const char* const * items,
-                   int menu_only, int initial_selection) {
+                   int menu_only, int initial_selection, Device* device) {
     // throw away keys pressed previously, so user doesn't
     // accidentally trigger menu items.
     ui->FlushKeys();
@@ -444,26 +444,26 @@
             } else {
                 LOGI("timed out waiting for key input; rebooting.\n");
                 ui->EndMenu();
-                return ITEM_REBOOT;
+                return 0; // XXX fixme
             }
         }
 
-        int action = device_handle_key(key, visible);
+        int action = device->HandleMenuKey(key, visible);
 
         if (action < 0) {
             switch (action) {
-                case HIGHLIGHT_UP:
+                case Device::kHighlightUp:
                     --selected;
                     selected = ui->SelectMenu(selected);
                     break;
-                case HIGHLIGHT_DOWN:
+                case Device::kHighlightDown:
                     ++selected;
                     selected = ui->SelectMenu(selected);
                     break;
-                case SELECT_ITEM:
+                case Device::kInvokeItem:
                     chosen_item = selected;
                     break;
-                case NO_ACTION:
+                case Device::kNoAction:
                     break;
             }
         } else if (!menu_only) {
@@ -481,7 +481,7 @@
 
 static int
 update_directory(const char* path, const char* unmount_when_done,
-                 int* wipe_cache) {
+                 int* wipe_cache, Device* device) {
     ensure_path_mounted(path);
 
     const char* MENU_HEADERS[] = { "Choose a package to install:",
@@ -555,7 +555,7 @@
     int result;
     int chosen_item = 0;
     do {
-        chosen_item = get_menu_selection(headers, zips, 1, chosen_item);
+        chosen_item = get_menu_selection(headers, zips, 1, chosen_item, device);
 
         char* item = zips[chosen_item];
         int item_len = strlen(item);
@@ -570,7 +570,7 @@
             strlcat(new_path, "/", PATH_MAX);
             strlcat(new_path, item, PATH_MAX);
             new_path[strlen(new_path)-1] = '\0';  // truncate the trailing '/'
-            result = update_directory(new_path, unmount_when_done, wipe_cache);
+            result = update_directory(new_path, unmount_when_done, wipe_cache, device);
             if (result >= 0) break;
         } else {
             // selected a zip file:  attempt to install it, and return
@@ -608,7 +608,7 @@
 }
 
 static void
-wipe_data(int confirm) {
+wipe_data(int confirm, Device* device) {
     if (confirm) {
         static const char** title_headers = NULL;
 
@@ -633,54 +633,54 @@
                                 " No",
                                 NULL };
 
-        int chosen_item = get_menu_selection(title_headers, items, 1, 0);
+        int chosen_item = get_menu_selection(title_headers, items, 1, 0, device);
         if (chosen_item != 7) {
             return;
         }
     }
 
     ui->Print("\n-- Wiping data...\n");
-    device_wipe_data();
+    device->WipeData();
     erase_volume("/data");
     erase_volume("/cache");
     ui->Print("Data wipe complete.\n");
 }
 
 static void
-prompt_and_wait() {
-    const char** headers = prepend_title((const char**)MENU_HEADERS);
+prompt_and_wait(Device* device) {
+    const char* const* headers = prepend_title(device->GetMenuHeaders());
 
     for (;;) {
         finish_recovery(NULL);
         ui->SetProgressType(RecoveryUI::EMPTY);
 
-        int chosen_item = get_menu_selection(headers, MENU_ITEMS, 0, 0);
+        int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);
 
         // device-specific code may take some action here.  It may
         // return one of the core actions handled in the switch
         // statement below.
-        chosen_item = device_perform_action(chosen_item);
+        chosen_item = device->InvokeMenuItem(chosen_item);
 
         int status;
         int wipe_cache;
         switch (chosen_item) {
-            case ITEM_REBOOT:
+            case Device::REBOOT:
                 return;
 
-            case ITEM_WIPE_DATA:
-                wipe_data(ui->IsTextVisible());
+            case Device::WIPE_DATA:
+                wipe_data(ui->IsTextVisible(), device);
                 if (!ui->IsTextVisible()) return;
                 break;
 
-            case ITEM_WIPE_CACHE:
+            case Device::WIPE_CACHE:
                 ui->Print("\n-- Wiping cache...\n");
                 erase_volume("/cache");
                 ui->Print("Cache wipe complete.\n");
                 if (!ui->IsTextVisible()) return;
                 break;
 
-            case ITEM_APPLY_SDCARD:
-                status = update_directory(SDCARD_ROOT, SDCARD_ROOT, &wipe_cache);
+            case Device::APPLY_EXT:
+                status = update_directory(SDCARD_ROOT, SDCARD_ROOT, &wipe_cache, device);
                 if (status == INSTALL_SUCCESS && wipe_cache) {
                     ui->Print("\n-- Wiping cache (at package request)...\n");
                     if (erase_volume("/cache")) {
@@ -700,9 +700,10 @@
                     }
                 }
                 break;
-            case ITEM_APPLY_CACHE:
+
+            case Device::APPLY_CACHE:
                 // Don't unmount cache at the end of this.
-                status = update_directory(CACHE_ROOT, NULL, &wipe_cache);
+                status = update_directory(CACHE_ROOT, NULL, &wipe_cache, device);
                 if (status == INSTALL_SUCCESS && wipe_cache) {
                     ui->Print("\n-- Wiping cache (at package request)...\n");
                     if (erase_volume("/cache")) {
@@ -722,7 +723,6 @@
                     }
                 }
                 break;
-
         }
     }
 }
@@ -741,10 +741,8 @@
     freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);
     printf("Starting recovery on %s", ctime(&start));
 
-    // TODO: device_* should be a C++ class; init should return the
-    // appropriate UI for the device.
-    device_ui_init(&ui_parameters);
-    ui = new ScreenRecoveryUI();
+    Device* device = make_device();
+    ui = device->GetUI();
 
     ui->Init();
     ui->SetBackground(RecoveryUI::INSTALLING);
@@ -771,7 +769,7 @@
         }
     }
 
-    device_recovery_start();
+    device->StartRecovery();
 
     printf("Command:");
     for (arg = 0; arg < argc; arg++) {
@@ -809,7 +807,7 @@
         }
         if (status != INSTALL_SUCCESS) ui->Print("Installation aborted.\n");
     } else if (wipe_data) {
-        if (device_wipe_data()) status = INSTALL_ERROR;
+        if (device->WipeData()) status = INSTALL_ERROR;
         if (erase_volume("/data")) status = INSTALL_ERROR;
         if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
         if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
@@ -822,7 +820,7 @@
 
     if (status != INSTALL_SUCCESS) ui->SetBackground(RecoveryUI::ERROR);
     if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
-        prompt_and_wait();
+        prompt_and_wait(device);
     }
 
     // Otherwise, get ready to boot the main system...
diff --git a/recovery_ui.h b/recovery_ui.h
deleted file mode 100644
index 4c4baf5..0000000
--- a/recovery_ui.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2009 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.
- */
-
-#ifndef _RECOVERY_UI_H
-#define _RECOVERY_UI_H
-
-#include "common.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-// Called before UI library is initialized.  Can change things like
-// how many frames are included in various animations, etc.
-extern void device_ui_init(UIParameters* ui_parameters);
-
-// Called when recovery starts up.  Returns 0.
-extern int device_recovery_start();
-
-// Called in the input thread when a new key (key_code) is pressed.
-// *key_pressed is an array of KEY_MAX+1 bytes indicating which other
-// keys are already pressed.  Return true if the text display should
-// be toggled.
-extern int device_toggle_display(volatile char* key_pressed, int key_code);
-
-// Called in the input thread when a new key (key_code) is pressed.
-// *key_pressed is an array of KEY_MAX+1 bytes indicating which other
-// keys are already pressed.  Return true if the device should reboot
-// immediately.
-extern int device_reboot_now(volatile char* key_pressed, int key_code);
-
-// Called from the main thread when recovery is waiting for input and
-// a key is pressed.  key is the code of the key pressed; visible is
-// true if the recovery menu is being shown.  Implementations can call
-// ui_key_pressed() to discover if other keys are being held down.
-// Return one of the defined constants below in order to:
-//
-//   - move the menu highlight (HIGHLIGHT_*)
-//   - invoke the highlighted item (SELECT_ITEM)
-//   - do nothing (NO_ACTION)
-//   - invoke a specific action (a menu position: any non-negative number)
-extern int device_handle_key(int key, int visible);
-
-// Perform a recovery action selected from the menu.  'which' will be
-// the item number of the selected menu item, or a non-negative number
-// returned from device_handle_key().  The menu will be hidden when
-// this is called; implementations can call ui_print() to print
-// information to the screen.
-extern int device_perform_action(int which);
-
-// Called when we do a wipe data/factory reset operation (either via a
-// reboot from the main system with the --wipe_data flag, or when the
-// user boots into recovery manually and selects the option from the
-// menu.)  Can perform whatever device-specific wiping actions are
-// needed.  Return 0 on success.  The userdata and cache partitions
-// are erased after this returns (whether it returns success or not).
-int device_wipe_data();
-
-#define NO_ACTION           -1
-
-#define HIGHLIGHT_UP        -2
-#define HIGHLIGHT_DOWN      -3
-#define SELECT_ITEM         -4
-
-#define ITEM_REBOOT          0
-#define ITEM_APPLY_EXT       1
-#define ITEM_APPLY_SDCARD    1  // historical synonym for ITEM_APPLY_EXT
-#define ITEM_WIPE_DATA       2
-#define ITEM_WIPE_CACHE      3
-#define ITEM_APPLY_CACHE     4
-
-// Header text to display above the main menu.
-extern char* MENU_HEADERS[];
-
-// Text of menu items.
-extern char* MENU_ITEMS[];
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/screen_ui.cpp b/screen_ui.cpp
index e6a31db..a60b046 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -31,9 +31,9 @@
 #include "common.h"
 #include <cutils/android_reboot.h>
 #include "minui/minui.h"
-#include "recovery_ui.h"
 #include "ui.h"
 #include "screen_ui.h"
+#include "device.h"
 
 #define CHAR_WIDTH 10
 #define CHAR_HEIGHT 18
@@ -78,7 +78,8 @@
     menu_top(0),
     menu_items(0),
     menu_sel(0),
-    key_queue_len(0) {
+    key_queue_len(0),
+    key_last_down(-1) {
     pthread_mutex_init(&updateMutex, NULL);
     pthread_mutex_init(&key_queue_mutex, NULL);
     pthread_cond_init(&key_queue_cond, NULL);
@@ -281,7 +282,6 @@
 {
     struct input_event ev;
     int ret;
-    int fake_key = 0;
 
     ret = ev_get_input(fd, revents, &ev);
     if (ret)
@@ -297,16 +297,12 @@
             // key event.
             self->rel_sum += ev.value;
             if (self->rel_sum > 3) {
-                fake_key = 1;
-                ev.type = EV_KEY;
-                ev.code = KEY_DOWN;
-                ev.value = 1;
+                self->process_key(KEY_DOWN, 1);   // press down key
+                self->process_key(KEY_DOWN, 0);   // and release it
                 self->rel_sum = 0;
             } else if (self->rel_sum < -3) {
-                fake_key = 1;
-                ev.type = EV_KEY;
-                ev.code = KEY_UP;
-                ev.value = 1;
+                self->process_key(KEY_UP, 1);     // press up key
+                self->process_key(KEY_UP, 0);     // and release it
                 self->rel_sum = 0;
             }
         }
@@ -314,38 +310,65 @@
         self->rel_sum = 0;
     }
 
-    if (ev.type != EV_KEY || ev.code > KEY_MAX)
-        return 0;
-
-    pthread_mutex_lock(&self->key_queue_mutex);
-    if (!fake_key) {
-        // our "fake" keys only report a key-down event (no
-        // key-up), so don't record them in the key_pressed
-        // table.
-        self->key_pressed[ev.code] = ev.value;
-    }
-    const int queue_max = sizeof(self->key_queue) / sizeof(self->key_queue[0]);
-    if (ev.value > 0 && self->key_queue_len < queue_max) {
-        self->key_queue[self->key_queue_len++] = ev.code;
-        pthread_cond_signal(&self->key_queue_cond);
-    }
-    pthread_mutex_unlock(&self->key_queue_mutex);
-
-    if (ev.value > 0 && device_toggle_display(self->key_pressed, ev.code)) {
-        pthread_mutex_lock(&self->updateMutex);
-        self->show_text = !self->show_text;
-        if (self->show_text) self->show_text_ever = true;
-        self->update_screen_locked();
-        pthread_mutex_unlock(&self->updateMutex);
-    }
-
-    if (ev.value > 0 && device_reboot_now(self->key_pressed, ev.code)) {
-        android_reboot(ANDROID_RB_RESTART, 0, 0);
-    }
+    if (ev.type == EV_KEY && ev.code <= KEY_MAX)
+        self->process_key(ev.code, ev.value);
 
     return 0;
 }
 
+// Process a key-up or -down event.  A key is "registered" when it is
+// pressed and then released, with no other keypresses or releases in
+// between.  Registered keys are passed to CheckKey() to see if it
+// should trigger a visibility toggle, an immediate reboot, or be
+// queued to be processed next time the foreground thread wants a key
+// (eg, for the menu).
+//
+// We also keep track of which keys are currently down so that
+// CheckKey can call IsKeyPressed to see what other keys are held when
+// a key is registered.
+//
+// updown == 1 for key down events; 0 for key up events
+void ScreenRecoveryUI::process_key(int key_code, int updown) {
+    bool register_key = false;
+
+    pthread_mutex_lock(&key_queue_mutex);
+    key_pressed[key_code] = updown;
+    if (updown) {
+        key_last_down = key_code;
+    } else {
+        if (key_last_down == key_code)
+            register_key = true;
+        key_last_down = -1;
+    }
+    pthread_mutex_unlock(&key_queue_mutex);
+
+    if (register_key) {
+        switch (CheckKey(key_code)) {
+          case RecoveryUI::TOGGLE:
+            pthread_mutex_lock(&updateMutex);
+            show_text = !show_text;
+            if (show_text) show_text_ever = true;
+            update_screen_locked();
+            pthread_mutex_unlock(&updateMutex);
+            break;
+
+          case RecoveryUI::REBOOT:
+            android_reboot(ANDROID_RB_RESTART, 0, 0);
+            break;
+
+          case RecoveryUI::ENQUEUE:
+            pthread_mutex_lock(&key_queue_mutex);
+            const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
+            if (key_queue_len < queue_max) {
+                key_queue[key_queue_len++] = key_code;
+                pthread_cond_signal(&key_queue_cond);
+            }
+            pthread_mutex_unlock(&key_queue_mutex);
+            break;
+        }
+    }
+}
+
 // Reads input events, handles special hot keys, and adds to the key queue.
 void* ScreenRecoveryUI::input_thread(void *cookie)
 {
@@ -622,8 +645,10 @@
 
 bool ScreenRecoveryUI::IsKeyPressed(int key)
 {
-    // This is a volatile static array, don't bother locking
-    return key_pressed[key];
+    pthread_mutex_lock(&key_queue_mutex);
+    int pressed = key_pressed[key];
+    pthread_mutex_unlock(&key_queue_mutex);
+    return pressed;
 }
 
 void ScreenRecoveryUI::FlushKeys() {
@@ -631,3 +656,7 @@
     key_queue_len = 0;
     pthread_mutex_unlock(&key_queue_mutex);
 }
+
+RecoveryUI::KeyAction ScreenRecoveryUI::CheckKey(int key) {
+    return RecoveryUI::ENQUEUE;
+}
diff --git a/screen_ui.h b/screen_ui.h
index 544f154..a5ec0d3 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -47,6 +47,10 @@
     int WaitKey();
     bool IsKeyPressed(int key);
     void FlushKeys();
+    // The default implementation of CheckKey enqueues all keys.
+    // Devices should typically override this to provide some way to
+    // toggle the log/menu display, and to do an immediate reboot.
+    KeyAction CheckKey(int key);
 
     // printing messages
     void Print(const char* fmt, ...); // __attribute__((format(printf, 1, 2)));
@@ -95,7 +99,8 @@
     pthread_mutex_t key_queue_mutex;
     pthread_cond_t key_queue_cond;
     int key_queue[256], key_queue_len;
-    volatile char key_pressed[KEY_MAX + 1];
+    char key_pressed[KEY_MAX + 1];     // under key_queue_mutex
+    int key_last_down;                 // under key_queue_mutex
     int rel_sum;
 
     pthread_t progress_t;
@@ -110,6 +115,7 @@
     void update_progress_locked();
     static void* progress_thread(void* cookie);
     static int input_callback(int fd, short revents, void* data);
+    void process_key(int key_code, int updown);
     static void* input_thread(void* cookie);
 
     bool usb_connected();
diff --git a/ui.h b/ui.h
index 6150bfd..3ca99a6 100644
--- a/ui.h
+++ b/ui.h
@@ -64,6 +64,13 @@
     // Erase any queued-up keys.
     virtual void FlushKeys() = 0;
 
+    // Called on each keypress, even while operations are in progress.
+    // Return value indicates whether an immediate operation should be
+    // triggered (toggling the display, rebooting the device), or if
+    // the key should be enqueued for use by the main thread.
+    enum KeyAction { ENQUEUE, TOGGLE, REBOOT };
+    virtual KeyAction CheckKey(int key) = 0;
+
     // --- menu display ---
 
     // Display some header text followed by a menu of items, which appears
diff --git a/updater/install.c b/updater/install.c
index 7b4b99b..f68bd03 100644
--- a/updater/install.c
+++ b/updater/install.c
@@ -792,11 +792,12 @@
         return NULL;
     }
 
+    char* partition = NULL;
     if (partition_value->type != VAL_STRING) {
         ErrorAbort(state, "partition argument to %s must be string", name);
         goto done;
     }
-    char* partition = partition_value->data;
+    partition = partition_value->data;
     if (strlen(partition) == 0) {
         ErrorAbort(state, "partition argument to %s can't be empty", name);
         goto done;