auto import from //branches/cupcake/...@130745
diff --git a/common.h b/common.h
index 76d5747..e17f76a 100644
--- a/common.h
+++ b/common.h
@@ -26,12 +26,24 @@
 int ui_wait_key();            // waits for a key/button press, returns the code
 int ui_key_pressed(int key);  // returns >0 if the code is currently pressed
 int ui_text_visible();        // returns >0 if text log is currently visible
+void ui_clear_key_queue();
 
 // Write a message to the on-screen log shown with Alt-L (also to stderr).
 // The screen is small, and users may need to report these messages to support,
 // so keep the output short and not too cryptic.
 void ui_print(const char *fmt, ...);
 
+// Display some header text followed by a menu of items, which appears
+// at the top of the screen (in place of any scrolling ui_print()
+// output, if necessary).
+void ui_start_menu(char** headers, char** items);
+// Set the menu highlight to the given index, and return it (capped to
+// the range [0..numitems).
+int ui_menu_select(int sel);
+// End menu mode, resetting the text overlay so that ui_print()
+// statements will be displayed.
+void ui_end_menu();
+
 // Set the icon (normally the only thing visible besides the progress bar).
 enum {
   BACKGROUND_ICON_NONE,
diff --git a/minui/graphics.c b/minui/graphics.c
index 05e8763..06c5fdf 100644
--- a/minui/graphics.c
+++ b/minui/graphics.c
@@ -43,6 +43,7 @@
 static GGLContext *gr_context = 0;
 static GGLSurface gr_font_texture;
 static GGLSurface gr_framebuffer[2];
+static GGLSurface gr_mem_surface;
 static unsigned gr_active_fb = 0;
 
 static int gr_fb_fd = -1;
@@ -55,7 +56,7 @@
     int fd;
     struct fb_fix_screeninfo fi;
     void *bits;
-    
+
     fd = open("/dev/graphics/fb0", O_RDWR);
     if (fd < 0) {
         perror("cannot open fb0");
@@ -87,9 +88,9 @@
     fb->stride = vi.xres;
     fb->data = bits;
     fb->format = GGL_PIXEL_FORMAT_RGB_565;
-    
+
     fb++;
-    
+
     fb->version = sizeof(*fb);
     fb->width = vi.xres;
     fb->height = vi.yres;
@@ -100,6 +101,15 @@
     return fd;
 }
 
+static void get_memory_surface(GGLSurface* ms) {
+  ms->version = sizeof(*ms);
+  ms->width = vi.xres;
+  ms->height = vi.yres;
+  ms->stride = vi.xres;
+  ms->data = malloc(vi.xres * vi.yres * 2);
+  ms->format = GGL_PIXEL_FORMAT_RGB_565;
+}
+
 static void set_active_framebuffer(unsigned n)
 {
     if (n > 1) return;
@@ -113,14 +123,16 @@
 void gr_flip(void)
 {
     GGLContext *gl = gr_context;
-    
-        /* currently active buffer becomes the backbuffer */
-    gl->colorBuffer(gl, gr_framebuffer + gr_active_fb);
 
-        /* swap front and back buffers */
+    /* swap front and back buffers */
     gr_active_fb = (gr_active_fb + 1) & 1;
 
-        /* inform the display driver */
+    /* copy data from the in-memory surface to the buffer we're about
+     * to make active. */
+    memcpy(gr_framebuffer[gr_active_fb].data, gr_mem_surface.data,
+           vi.xres * vi.yres * 2);
+
+    /* inform the display driver */
     set_active_framebuffer(gr_active_fb);
 }
 
@@ -147,13 +159,13 @@
     unsigned off;
 
     y -= font->ascent;
-  
+
     gl->bindTexture(gl, &font->texture);
     gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
     gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
     gl->texGeni(gl, GGL_T, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
     gl->enable(gl, GGL_TEXTURE_2D);
-    
+
     while((off = *s++)) {
         off -= 32;
         if (off < 96) {
@@ -209,10 +221,10 @@
     unsigned char *in, data;
 
     gr_font = calloc(sizeof(*gr_font), 1);
-    ftex = &gr_font->texture; 
+    ftex = &gr_font->texture;
 
     bits = malloc(font.width * font.height);
-    
+
     ftex->version = sizeof(*ftex);
     ftex->width = font.width;
     ftex->height = font.height;
@@ -225,7 +237,7 @@
         memset(bits, (data & 0x80) ? 255 : 0, data & 0x7f);
         bits += (data & 0x7f);
     }
-    
+
     gr_font->cwidth = font.cwidth;
     gr_font->cheight = font.cheight;
     gr_font->ascent = font.cheight - 2;
@@ -254,13 +266,16 @@
         return -1;
     }
 
+    get_memory_surface(&gr_mem_surface);
+
     fprintf(stderr, "framebuffer: fd %d (%d x %d)\n",
             gr_fb_fd, gr_framebuffer[0].width, gr_framebuffer[0].height);
 
         /* start with 0 as front (displayed) and 1 as back (drawing) */
     gr_active_fb = 0;
     set_active_framebuffer(0);
-    gl->colorBuffer(gl, gr_framebuffer + 1);
+    gl->colorBuffer(gl, &gr_mem_surface);
+
 
     gl->activeTexture(gl, 0);
     gl->enable(gl, GGL_BLEND);
@@ -273,7 +288,9 @@
 {
     close(gr_fb_fd);
     gr_fb_fd = -1;
-    
+
+    free(gr_mem_surface.data);
+
     ioctl(gr_vt_fd, KDSETMODE, (void*) KD_TEXT);
     close(gr_vt_fd);
     gr_vt_fd = -1;
@@ -291,5 +308,5 @@
 
 gr_pixel *gr_fb_data(void)
 {
-    return (unsigned short *) gr_framebuffer[1 - gr_active_fb].data;
+    return (unsigned short *) gr_mem_surface.data;
 }
diff --git a/recovery.c b/recovery.c
index 3d44df3..221ee29 100644
--- a/recovery.c
+++ b/recovery.c
@@ -291,17 +291,32 @@
 static void
 prompt_and_wait()
 {
-    ui_print("\n"
-        "Home+Back - reboot system now\n"
-        "Alt+L - toggle log text display\n"
-        "Alt+S - apply sdcard:update.zip\n"
-        "Alt+W - wipe data/factory reset\n");
+    char* headers[] = { "Android system recovery utility",
+                        "",
+                        "Use trackball to highlight;",
+                        "click to select.",
+                        "",
+                        NULL };
 
+    // these constants correspond to elements of the items[] list.
+#define ITEM_REBOOT        0
+#define ITEM_APPLY_SDCARD  1
+#define ITEM_WIPE_DATA     2
+    char* items[] = { "reboot system now [Home+Back]",
+                      "apply sdcard:update.zip [Alt+S]",
+                      "wipe data/factory reset [Alt+W]",
+                      NULL };
+
+    ui_start_menu(headers, items);
+    int selected = 0;
+    int chosen_item = -1;
+
+    finish_recovery(NULL);
+    ui_reset_progress();
     for (;;) {
-        finish_recovery(NULL);
-        ui_reset_progress();
         int key = ui_wait_key();
         int alt = ui_key_pressed(KEY_LEFTALT) || ui_key_pressed(KEY_RIGHTALT);
+        int visible = ui_text_visible();
 
         if (key == KEY_DREAM_BACK && ui_key_pressed(KEY_DREAM_HOME)) {
             // Wait for the keys to be released, to avoid triggering
@@ -310,23 +325,64 @@
                    ui_key_pressed(KEY_DREAM_HOME)) {
                 usleep(1000);
             }
-            break;
+            chosen_item = ITEM_REBOOT;
         } else if (alt && key == KEY_W) {
-            ui_print("\n");
-            erase_root("DATA:");
-            erase_root("CACHE:");
-            ui_print("Data wipe complete.\n");
-            if (!ui_text_visible()) break;
+            chosen_item = ITEM_WIPE_DATA;
         } else if (alt && key == KEY_S) {
-            ui_print("\nInstalling from sdcard...\n");
-            int status = install_package(SDCARD_PACKAGE_FILE);
-            if (status != INSTALL_SUCCESS) {
-                ui_set_background(BACKGROUND_ICON_ERROR);
-                ui_print("Installation aborted.\n");
-            } else if (!ui_text_visible()) {
-                break;  // reboot if logs aren't visible
+            chosen_item = ITEM_APPLY_SDCARD;
+        } else if ((key == KEY_DOWN || key == KEY_VOLUMEDOWN) && visible) {
+            ++selected;
+            selected = ui_menu_select(selected);
+        } else if ((key == KEY_UP || key == KEY_VOLUMEUP) && visible) {
+            --selected;
+            selected = ui_menu_select(selected);
+        } else if (key == BTN_MOUSE && visible) {
+            chosen_item = selected;
+        }
+
+        if (chosen_item >= 0) {
+            // turn off the menu, letting ui_print() to scroll output
+            // on the screen.
+            ui_end_menu();
+
+            switch (chosen_item) {
+                case ITEM_REBOOT:
+                    return;
+
+                case ITEM_WIPE_DATA:
+                    ui_print("\n-- Wiping data...\n");
+                    erase_root("DATA:");
+                    erase_root("CACHE:");
+                    ui_print("Data wipe complete.\n");
+                    if (!ui_text_visible()) return;
+                    break;
+
+                case ITEM_APPLY_SDCARD:
+                    ui_print("\n-- Install from sdcard...\n");
+                    int status = install_package(SDCARD_PACKAGE_FILE);
+                    if (status != INSTALL_SUCCESS) {
+                        ui_set_background(BACKGROUND_ICON_ERROR);
+                        ui_print("Installation aborted.\n");
+                    } else if (!ui_text_visible()) {
+                        return;  // reboot if logs aren't visible
+                    } else {
+                      ui_print("Install from sdcard complete.\n");
+                    }
+                    break;
             }
-            ui_print("\nPress Home+Back to reboot\n");
+
+            // if we didn't return from this function to reboot, show
+            // the menu again.
+            ui_start_menu(headers, items);
+            selected = 0;
+            chosen_item = -1;
+
+            finish_recovery(NULL);
+            ui_reset_progress();
+
+            // throw away keys pressed while the command was running,
+            // so user doesn't accidentally trigger menu items.
+            ui_clear_key_queue();
         }
     }
 }
@@ -348,7 +404,6 @@
     fprintf(stderr, "Starting recovery on %s", ctime(&start));
 
     ui_init();
-    ui_print("Android system recovery utility\n");
     get_args(&argc, &argv);
 
     int previous_runs = 0;
diff --git a/ui.c b/ui.c
index b9869ae..5d06561 100644
--- a/ui.c
+++ b/ui.c
@@ -89,6 +89,10 @@
 static int text_col = 0, text_row = 0, text_top = 0;
 static int show_text = 0;
 
+static char menu[MAX_ROWS][MAX_COLS];
+static int show_menu = 0;
+static int menu_top = 0, menu_items = 0, menu_sel = 0;
+
 // Key event input queue
 static pthread_mutex_t key_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
 static pthread_cond_t key_queue_cond = PTHREAD_COND_INITIALIZER;
@@ -154,6 +158,12 @@
     }
 }
 
+static void draw_text_line(int row, const char* t) {
+  if (t[0] != '\0') {
+    gr_text(0, (row+1)*CHAR_HEIGHT-1, t);
+  }
+}
+
 // Redraw everything on the screen.  Does not flip pages.
 // Should only be called with gUpdateMutex locked.
 static void draw_screen_locked(void)
@@ -163,13 +173,32 @@
 
     if (show_text) {
         gr_color(0, 0, 0, 160);
-        gr_fill(0, 0, gr_fb_width(), text_rows * CHAR_HEIGHT);
+        gr_fill(0, 0, gr_fb_width(), gr_fb_height());
+
+        int i = 0;
+        if (show_menu) {
+            gr_color(64, 96, 255, 255);
+            gr_fill(0, (menu_top+menu_sel) * CHAR_HEIGHT,
+                    gr_fb_width(), (menu_top+menu_sel+1)*CHAR_HEIGHT+1);
+
+            for (; i < menu_top + menu_items; ++i) {
+                if (i == menu_top + menu_sel) {
+                    gr_color(255, 255, 255, 255);
+                    draw_text_line(i, menu[i]);
+                    gr_color(64, 96, 255, 255);
+                } else {
+                    draw_text_line(i, menu[i]);
+                }
+            }
+            gr_fill(0, i*CHAR_HEIGHT+CHAR_HEIGHT/2-1,
+                    gr_fb_width(), i*CHAR_HEIGHT+CHAR_HEIGHT/2+1);
+            ++i;
+        }
 
         gr_color(255, 255, 0, 255);
-        int i;
-        for (i = 0; i < text_rows; ++i) {
-            const char* line = text[(i + text_top) % text_rows];
-            if (line[0] != '\0') gr_text(0, (i + 1) * CHAR_HEIGHT - 1, line);
+
+        for (; i < text_rows; ++i) {
+            draw_text_line(i, text[(i+text_top) % text_rows]);
         }
     }
 }
@@ -228,13 +257,50 @@
 // Reads input events, handles special hot keys, and adds to the key queue.
 static void *input_thread(void *cookie)
 {
+    int rel_sum = 0;
+    int fake_key = 0;
     for (;;) {
         // wait for the next key event
         struct input_event ev;
-        do { ev_get(&ev, 0); } while (ev.type != EV_KEY || ev.code > KEY_MAX);
+        do {
+            ev_get(&ev, 0);
+
+            if (ev.type == EV_SYN) {
+                continue;
+            } else if (ev.type == EV_REL) {
+                if (ev.code == REL_Y) {
+                    // accumulate the up or down motion reported by
+                    // the trackball.  When it exceeds a threshold
+                    // (positive or negative), fake an up/down
+                    // key event.
+                    rel_sum += ev.value;
+                    if (rel_sum > 3) {
+                        fake_key = 1;
+                        ev.type = EV_KEY;
+                        ev.code = KEY_DOWN;
+                        ev.value = 1;
+                        rel_sum = 0;
+                    } else if (rel_sum < -3) {
+                        fake_key = 1;
+                        ev.type = EV_KEY;
+                        ev.code = KEY_UP;
+                        ev.value = 1;
+                        rel_sum = 0;
+                    }
+                }
+            } else {
+                rel_sum = 0;
+            }
+        } while (ev.type != EV_KEY || ev.code > KEY_MAX);
 
         pthread_mutex_lock(&key_queue_mutex);
-        key_pressed[ev.code] = ev.value;
+        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.
+            key_pressed[ev.code] = ev.value;
+        }
+        fake_key = 0;
         const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
         if (ev.value > 0 && key_queue_len < queue_max) {
             key_queue[key_queue_len++] = ev.code;
@@ -242,9 +308,10 @@
         }
         pthread_mutex_unlock(&key_queue_mutex);
 
-        // Alt+L: toggle log display
+        // Alt+L or Home+End: toggle log display
         int alt = key_pressed[KEY_LEFTALT] || key_pressed[KEY_RIGHTALT];
-        if (alt && ev.code == KEY_L && ev.value > 0) {
+        if ((alt && ev.code == KEY_L && ev.value > 0) ||
+            (key_pressed[KEY_HOME] && ev.code == KEY_END && ev.value > 0)) {
             pthread_mutex_lock(&gUpdateMutex);
             show_text = !show_text;
             update_screen_locked();
@@ -269,6 +336,7 @@
     text_col = text_row = 0;
     text_rows = gr_fb_height() / CHAR_HEIGHT;
     if (text_rows > MAX_ROWS) text_rows = MAX_ROWS;
+    text_top = 1;
 
     text_cols = gr_fb_width() / CHAR_WIDTH;
     if (text_cols > MAX_COLS - 1) text_cols = MAX_COLS - 1;
@@ -392,6 +460,54 @@
     pthread_mutex_unlock(&gUpdateMutex);
 }
 
+void ui_start_menu(char** headers, char** items) {
+    int i;
+    pthread_mutex_lock(&gUpdateMutex);
+    if (text_rows > 0 && text_cols > 0) {
+        for (i = 0; i < text_rows; ++i) {
+            if (headers[i] == NULL) break;
+            strncpy(menu[i], headers[i], text_cols-1);
+            menu[i][text_cols-1] = '\0';
+        }
+        menu_top = i;
+        for (; i < text_rows; ++i) {
+            if (items[i-menu_top] == NULL) break;
+            strncpy(menu[i], items[i-menu_top], text_cols-1);
+            menu[i][text_cols-1] = '\0';
+        }
+        menu_items = i - menu_top;
+        show_menu = 1;
+        menu_sel = 0;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&gUpdateMutex);
+}
+
+int ui_menu_select(int sel) {
+    int old_sel;
+    pthread_mutex_lock(&gUpdateMutex);
+    if (show_menu > 0) {
+        old_sel = menu_sel;
+        menu_sel = sel;
+        if (menu_sel < 0) menu_sel = 0;
+        if (menu_sel >= menu_items) menu_sel = menu_items-1;
+        sel = menu_sel;
+        if (menu_sel != old_sel) update_screen_locked();
+    }
+    pthread_mutex_unlock(&gUpdateMutex);
+    return sel;
+}
+
+void ui_end_menu() {
+    int i;
+    pthread_mutex_lock(&gUpdateMutex);
+    if (show_menu > 0 && text_rows > 0 && text_cols > 0) {
+        show_menu = 0;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&gUpdateMutex);
+}
+
 int ui_text_visible()
 {
     pthread_mutex_lock(&gUpdateMutex);
@@ -418,3 +534,9 @@
     // This is a volatile static array, don't bother locking
     return key_pressed[key];
 }
+
+void ui_clear_key_queue() {
+    pthread_mutex_lock(&key_queue_mutex);
+    key_queue_len = 0;
+    pthread_mutex_unlock(&key_queue_mutex);
+}