make recovery UI images more general; allow for installation animation

Change some of the UI parameters (# of indeterminate progress bar
frames, fps, etc.) from #defined constants to variables that can be
set by the device-specific recovery_ui code (via a new function).

Support overlaying different images on top of the base installation
icon to animate it.  Make the FPS control more accurate.

Change-Id: I9268b389b7ea6b3ed9e0c7eae37baf4272e60edd
diff --git a/common.h b/common.h
index e6e8f85..cba4c86 100644
--- a/common.h
+++ b/common.h
@@ -107,4 +107,23 @@
                               // (that much).
 } Volume;
 
+typedef struct {
+    // number of frames in indeterminate progress bar animation
+    int indeterminate_frames;
+
+    // number of frames per second to try to maintain when animating
+    int update_fps;
+
+    // number of frames in installing animation.  may be zero for a
+    // static installation icon.
+    int installing_frames;
+
+    // the install icon is animated by drawing images containing the
+    // changing part over the base icon.  These specify the
+    // coordinates of the upper-left corner.
+    int install_overlay_offset_x;
+    int install_overlay_offset_y;
+
+} UIParameters;
+
 #endif  // RECOVERY_COMMON_H
diff --git a/default_recovery_ui.c b/default_recovery_ui.c
index bcba888..7c4017e 100644
--- a/default_recovery_ui.c
+++ b/default_recovery_ui.c
@@ -29,6 +29,9 @@
                        "wipe cache partition",
                        NULL };
 
+void device_ui_init(UIParameters* ui_parameters) {
+}
+
 int device_recovery_start() {
     return 0;
 }
diff --git a/minui/resources.c b/minui/resources.c
index 3d2c727..2170dca 100644
--- a/minui/resources.c
+++ b/minui/resources.c
@@ -49,6 +49,8 @@
     png_structp png_ptr = NULL;
     png_infop info_ptr = NULL;
 
+    *pSurface = NULL;
+
     snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.png", name);
     resPath[sizeof(resPath)-1] = '\0';
     FILE* fp = fopen(resPath, "rb");
diff --git a/recovery.c b/recovery.c
index 671cfbe..14fc905 100644
--- a/recovery.c
+++ b/recovery.c
@@ -56,6 +56,8 @@
 static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
 static const char *SIDELOAD_TEMP_DIR = "/tmp/sideload";
 
+extern UIParameters ui_parameters;    // from ui.c
+
 /*
  * The recovery tool communicates with the main system through /cache files.
  *   /cache/recovery/command - INPUT - command line for tool, one arg per line
@@ -688,6 +690,7 @@
     freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);
     printf("Starting recovery on %s", ctime(&start));
 
+    device_ui_init(&ui_parameters);
     ui_init();
     ui_set_background(BACKGROUND_ICON_INSTALLING);
     load_volume_table();
diff --git a/recovery_ui.h b/recovery_ui.h
index 77ce7f9..e56a24b 100644
--- a/recovery_ui.h
+++ b/recovery_ui.h
@@ -17,6 +17,12 @@
 #ifndef _RECOVERY_UI_H
 #define _RECOVERY_UI_H
 
+#include "common.h"
+
+// 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();
 
diff --git a/ui.c b/ui.c
index 82004f0..054e53f 100644
--- a/ui.c
+++ b/ui.c
@@ -36,32 +36,32 @@
 #define CHAR_WIDTH 10
 #define CHAR_HEIGHT 18
 
-#define PROGRESSBAR_INDETERMINATE_STATES 6
-#define PROGRESSBAR_INDETERMINATE_FPS 15
-
 #define UI_WAIT_KEY_TIMEOUT_SEC    120
 
+UIParameters ui_parameters = {
+    6,     // indeterminate progress bar frames
+    15,    // fps
+    0,     // installation icon frames (0 == static image)
+    0, 0,  // installation icon overlay offset
+};
+
 static pthread_mutex_t gUpdateMutex = PTHREAD_MUTEX_INITIALIZER;
 static gr_surface gBackgroundIcon[NUM_BACKGROUND_ICONS];
-static gr_surface gProgressBarIndeterminate[PROGRESSBAR_INDETERMINATE_STATES];
+static gr_surface *gInstallationOverlay;
+static gr_surface *gProgressBarIndeterminate;
 static gr_surface gProgressBarEmpty;
 static gr_surface gProgressBarFill;
 
 static const struct { gr_surface* surface; const char *name; } BITMAPS[] = {
     { &gBackgroundIcon[BACKGROUND_ICON_INSTALLING], "icon_installing" },
     { &gBackgroundIcon[BACKGROUND_ICON_ERROR],      "icon_error" },
-    { &gProgressBarIndeterminate[0],    "indeterminate1" },
-    { &gProgressBarIndeterminate[1],    "indeterminate2" },
-    { &gProgressBarIndeterminate[2],    "indeterminate3" },
-    { &gProgressBarIndeterminate[3],    "indeterminate4" },
-    { &gProgressBarIndeterminate[4],    "indeterminate5" },
-    { &gProgressBarIndeterminate[5],    "indeterminate6" },
     { &gProgressBarEmpty,               "progress_empty" },
     { &gProgressBarFill,                "progress_fill" },
     { NULL,                             NULL },
 };
 
-static gr_surface gCurrentIcon = NULL;
+static int gCurrentIcon = 0;
+static int gInstallingFrame = 0;
 
 static enum ProgressBarType {
     PROGRESSBAR_TYPE_NONE,
@@ -71,7 +71,7 @@
 
 // Progress bar scope of current operation
 static float gProgressScopeStart = 0, gProgressScopeSize = 0, gProgress = 0;
-static time_t gProgressScopeTime, gProgressScopeDuration;
+static double gProgressScopeTime, gProgressScopeDuration;
 
 // Set to 1 when both graphics pages are the same (except for the progress bar)
 static int gPagesIdentical = 0;
@@ -93,20 +93,46 @@
 static int key_queue[256], key_queue_len = 0;
 static volatile char key_pressed[KEY_MAX + 1];
 
+// Return the current time as a double (including fractions of a second).
+static double now() {
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return tv.tv_sec + tv.tv_usec / 1000000.0;
+}
+
+// Draw the given frame over the installation overlay animation.  The
+// background is not cleared or draw with the base icon first; we
+// assume that the frame already contains some other frame of the
+// animation.  Does nothing if no overlay animation is defined.
+// Should only be called with gUpdateMutex locked.
+static void draw_install_overlay_locked(int frame) {
+    if (gInstallationOverlay == NULL) return;
+    gr_surface surface = gInstallationOverlay[frame];
+    int iconWidth = gr_get_width(surface);
+    int iconHeight = gr_get_height(surface);
+    gr_blit(surface, 0, 0, iconWidth, iconHeight,
+            ui_parameters.install_overlay_offset_x,
+            ui_parameters.install_overlay_offset_y);
+}
+
 // Clear the screen and draw the currently selected background icon (if any).
 // Should only be called with gUpdateMutex locked.
-static void draw_background_locked(gr_surface icon)
+static void draw_background_locked(int icon)
 {
     gPagesIdentical = 0;
     gr_color(0, 0, 0, 255);
     gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
     if (icon) {
-        int iconWidth = gr_get_width(icon);
-        int iconHeight = gr_get_height(icon);
+        gr_surface surface = gBackgroundIcon[icon];
+        int iconWidth = gr_get_width(surface);
+        int iconHeight = gr_get_height(surface);
         int iconX = (gr_fb_width() - iconWidth) / 2;
         int iconY = (gr_fb_height() - iconHeight) / 2;
-        gr_blit(icon, 0, 0, iconWidth, iconHeight, iconX, iconY);
+        gr_blit(surface, 0, 0, iconWidth, iconHeight, iconX, iconY);
+        if (icon == BACKGROUND_ICON_INSTALLING) {
+            draw_install_overlay_locked(gInstallingFrame);
+        }
     }
 }
 
@@ -114,35 +140,39 @@
 // Should only be called with gUpdateMutex locked.
 static void draw_progress_locked()
 {
-    if (gProgressBarType == PROGRESSBAR_TYPE_NONE) return;
-
-    int iconHeight = gr_get_height(gBackgroundIcon[BACKGROUND_ICON_INSTALLING]);
-    int width = gr_get_width(gProgressBarEmpty);
-    int height = gr_get_height(gProgressBarEmpty);
-
-    int dx = (gr_fb_width() - width)/2;
-    int dy = (3*gr_fb_height() + iconHeight - 2*height)/4;
-
-    // Erase behind the progress bar (in case this was a progress-only update)
-    gr_color(0, 0, 0, 255);
-    gr_fill(dx, dy, width, height);
-
-    if (gProgressBarType == PROGRESSBAR_TYPE_NORMAL) {
-        float progress = gProgressScopeStart + gProgress * gProgressScopeSize;
-        int pos = (int) (progress * width);
-
-        if (pos > 0) {
-          gr_blit(gProgressBarFill, 0, 0, pos, height, dx, dy);
-        }
-        if (pos < width-1) {
-          gr_blit(gProgressBarEmpty, pos, 0, width-pos, height, dx+pos, dy);
-        }
+    if (gCurrentIcon == BACKGROUND_ICON_INSTALLING) {
+        draw_install_overlay_locked(gInstallingFrame);
     }
 
-    if (gProgressBarType == PROGRESSBAR_TYPE_INDETERMINATE) {
-        static int frame = 0;
-        gr_blit(gProgressBarIndeterminate[frame], 0, 0, width, height, dx, dy);
-        frame = (frame + 1) % PROGRESSBAR_INDETERMINATE_STATES;
+    if (gProgressBarType != PROGRESSBAR_TYPE_NONE) {
+        int iconHeight = gr_get_height(gBackgroundIcon[BACKGROUND_ICON_INSTALLING]);
+        int width = gr_get_width(gProgressBarEmpty);
+        int height = gr_get_height(gProgressBarEmpty);
+
+        int dx = (gr_fb_width() - width)/2;
+        int dy = (3*gr_fb_height() + iconHeight - 2*height)/4;
+
+        // Erase behind the progress bar (in case this was a progress-only update)
+        gr_color(0, 0, 0, 255);
+        gr_fill(dx, dy, width, height);
+
+        if (gProgressBarType == PROGRESSBAR_TYPE_NORMAL) {
+            float progress = gProgressScopeStart + gProgress * gProgressScopeSize;
+            int pos = (int) (progress * width);
+
+            if (pos > 0) {
+                gr_blit(gProgressBarFill, 0, 0, pos, height, dx, dy);
+            }
+            if (pos < width-1) {
+                gr_blit(gProgressBarEmpty, pos, 0, width-pos, height, dx+pos, dy);
+            }
+        }
+
+        if (gProgressBarType == PROGRESSBAR_TYPE_INDETERMINATE) {
+            static int frame = 0;
+            gr_blit(gProgressBarIndeterminate[frame], 0, 0, width, height, dx, dy);
+            frame = (frame + 1) % ui_parameters.indeterminate_frames;
+        }
     }
 }
 
@@ -207,7 +237,7 @@
         draw_screen_locked();    // Must redraw the whole screen
         gPagesIdentical = 1;
     } else {
-        draw_progress_locked();  // Draw only the progress bar
+        draw_progress_locked();  // Draw only the progress bar and overlays
     }
     gr_flip();
 }
@@ -215,29 +245,49 @@
 // Keeps the progress bar updated, even when the process is otherwise busy.
 static void *progress_thread(void *cookie)
 {
+    double interval = 1.0 / ui_parameters.update_fps;
     for (;;) {
-        usleep(1000000 / PROGRESSBAR_INDETERMINATE_FPS);
+        double start = now();
         pthread_mutex_lock(&gUpdateMutex);
 
+        int redraw = 0;
+
+        // update the installation animation, if active
+        // skip this if we have a text overlay (too expensive to update)
+        if (gCurrentIcon == BACKGROUND_ICON_INSTALLING &&
+            ui_parameters.installing_frames > 0 &&
+            !show_text) {
+            gInstallingFrame =
+                (gInstallingFrame + 1) % ui_parameters.installing_frames;
+            redraw = 1;
+        }
+
         // update the progress bar animation, if active
         // skip this if we have a text overlay (too expensive to update)
         if (gProgressBarType == PROGRESSBAR_TYPE_INDETERMINATE && !show_text) {
-            update_progress_locked();
+            redraw = 1;
         }
 
         // move the progress bar forward on timed intervals, if configured
         int duration = gProgressScopeDuration;
         if (gProgressBarType == PROGRESSBAR_TYPE_NORMAL && duration > 0) {
-            int elapsed = time(NULL) - gProgressScopeTime;
+            double elapsed = now() - gProgressScopeTime;
             float progress = 1.0 * elapsed / duration;
             if (progress > 1.0) progress = 1.0;
             if (progress > gProgress) {
                 gProgress = progress;
-                update_progress_locked();
+                redraw = 1;
             }
         }
 
+        if (redraw) update_progress_locked();
+
         pthread_mutex_unlock(&gUpdateMutex);
+        double end = now();
+        // minimum of 20ms delay between frames
+        double delay = interval - (end-start);
+        if (delay < 0.02) delay = 0.02;
+        usleep((long)(delay * 1000000));
     }
     return NULL;
 }
@@ -328,15 +378,51 @@
     for (i = 0; BITMAPS[i].name != NULL; ++i) {
         int result = res_create_surface(BITMAPS[i].name, BITMAPS[i].surface);
         if (result < 0) {
-            if (result == -2) {
-                LOGI("Bitmap %s missing header\n", BITMAPS[i].name);
-            } else {
-                LOGE("Missing bitmap %s\n(Code %d)\n", BITMAPS[i].name, result);
-            }
-            *BITMAPS[i].surface = NULL;
+            LOGE("Missing bitmap %s\n(Code %d)\n", BITMAPS[i].name, result);
         }
     }
 
+    gProgressBarIndeterminate = malloc(ui_parameters.indeterminate_frames *
+                                       sizeof(gr_surface));
+    for (i = 0; i < ui_parameters.indeterminate_frames; ++i) {
+        char filename[40];
+        // "indeterminateN" if fewer than 10 frames, else "indeterminateNN".
+        sprintf(filename, "indeterminate%0*d",
+                ui_parameters.indeterminate_frames < 10 ? 1 : 2,
+                i+1);
+        int result = res_create_surface(filename, gProgressBarIndeterminate+i);
+        if (result < 0) {
+            LOGE("Missing bitmap %s\n(Code %d)\n", filename, result);
+        }
+    }
+
+    if (ui_parameters.installing_frames > 0) {
+        gInstallationOverlay = malloc(ui_parameters.installing_frames *
+                                      sizeof(gr_surface));
+        for (i = 0; i < ui_parameters.installing_frames; ++i) {
+            char filename[40];
+            sprintf(filename, "icon_installing_overlay%0*d",
+                    ui_parameters.installing_frames < 10 ? 1 : 2,
+                    i+1);
+            int result = res_create_surface(filename, gInstallationOverlay+i);
+            if (result < 0) {
+                LOGE("Missing bitmap %s\n(Code %d)\n", filename, result);
+            }
+        }
+
+        // Adjust the offset to account for the positioning of the
+        // base image on the screen.
+        if (gBackgroundIcon[BACKGROUND_ICON_INSTALLING] != NULL) {
+            gr_surface bg = gBackgroundIcon[BACKGROUND_ICON_INSTALLING];
+            ui_parameters.install_overlay_offset_x +=
+                (gr_fb_width() - gr_get_width(bg)) / 2;
+            ui_parameters.install_overlay_offset_y +=
+                (gr_fb_height() - gr_get_height(bg)) / 2;
+        }
+    } else {
+        gInstallationOverlay = NULL;
+    }
+
     pthread_t t;
     pthread_create(&t, NULL, progress_thread, NULL);
     pthread_create(&t, NULL, input_thread, NULL);
@@ -345,7 +431,7 @@
 void ui_set_background(int icon)
 {
     pthread_mutex_lock(&gUpdateMutex);
-    gCurrentIcon = gBackgroundIcon[icon];
+    gCurrentIcon = icon;
     update_screen_locked();
     pthread_mutex_unlock(&gUpdateMutex);
 }
@@ -366,7 +452,7 @@
     gProgressBarType = PROGRESSBAR_TYPE_NORMAL;
     gProgressScopeStart += gProgressScopeSize;
     gProgressScopeSize = portion;
-    gProgressScopeTime = time(NULL);
+    gProgressScopeTime = now();
     gProgressScopeDuration = seconds;
     gProgress = 0;
     update_progress_locked();