Detect update failures

This allows to delete the update only if the installation succeeded
and to notify the user in case the installation failed.

Since we now need the file in case of failure, create a copy for
uncrypt even if the user chose to delete installed updates.

Change-Id: I80b0f499663bbf50bcbca5f643c01ffdb4cd3957
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3efd798..bb6f50d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -41,6 +41,7 @@
     <string name="download_paused_error_notification">Download error</string>
     <string name="download_completed_notification">Download completed</string>
     <string name="download_starting_notification">Starting download</string>
+    <string name="update_failed_notification">Update failed</string>
 
     <string name="new_updates_found_title">New updates</string>
 
@@ -139,4 +140,5 @@
     <string name="export_channel_title">Export completion</string>
     <string name="new_updates_channel_title">New updates</string>
     <string name="ongoing_channel_title">Ongoing downloads</string>
+    <string name="update_failed_channel_title">Update failed</string>
 </resources>
diff --git a/src/org/lineageos/updater/UpdaterReceiver.java b/src/org/lineageos/updater/UpdaterReceiver.java
index ca09914..6ef4405 100644
--- a/src/org/lineageos/updater/UpdaterReceiver.java
+++ b/src/org/lineageos/updater/UpdaterReceiver.java
@@ -15,20 +15,75 @@
  */
 package org.lineageos.updater;
 
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.support.v4.app.NotificationCompat;
 import android.support.v7.preference.PreferenceManager;
 
+import org.lineageos.updater.misc.BuildInfoUtils;
 import org.lineageos.updater.misc.Constants;
+import org.lineageos.updater.misc.StringGenerator;
+
+import java.text.DateFormat;
 
 public class UpdaterReceiver extends BroadcastReceiver {
 
     public static final String ACTION_INSTALL_REBOOT =
             "org.lineageos.updater.action.INSTALL_REBOOT";
 
+    private static final String INSTALL_ERROR_NOTIFICATION_CHANNEL =
+            "install_error_notification_channel";
+
+    private static boolean shouldShowUpdateFailedNotification(Context context) {
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+        // We can't easily detect failed re-installations
+        if (preferences.getBoolean(Constants.PREF_INSTALL_AGAIN, false) ||
+                preferences.getBoolean(Constants.PREF_INSTALL_NOTIFIED, false)) {
+            return false;
+        }
+
+        long buildTimestamp = SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0);
+        long lastBuildTimestamp = preferences.getLong(Constants.PREF_INSTALL_OLD_TIMESTAMP, -1);
+        return buildTimestamp == lastBuildTimestamp;
+    }
+
+    private static void showUpdateFailedNotification(Context context) {
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+        String buildDate = StringGenerator.getDateLocalizedUTC(context,
+                DateFormat.MEDIUM, preferences.getLong(Constants.PREF_INSTALL_NEW_TIMESTAMP, 0));
+        String buildInfo = context.getString(R.string.list_build_version_date,
+                BuildInfoUtils.getBuildVersion(), buildDate);
+
+        Intent notificationIntent = new Intent(context, UpdatesActivity.class);
+        PendingIntent intent = PendingIntent.getActivity(context, 0, notificationIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+
+        NotificationChannel notificationChannel = new NotificationChannel(
+                INSTALL_ERROR_NOTIFICATION_CHANNEL,
+                context.getString(R.string.update_failed_channel_title),
+                NotificationManager.IMPORTANCE_LOW);
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(context,
+                INSTALL_ERROR_NOTIFICATION_CHANNEL)
+                .setContentIntent(intent)
+                .setSmallIcon(R.drawable.ic_system_update)
+                .setContentTitle(context.getString(R.string.update_failed_notification))
+                .setStyle(new NotificationCompat.BigTextStyle().bigText(buildInfo))
+                .setContentText(buildInfo);
+
+        NotificationManager nm = (NotificationManager) context.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        nm.createNotificationChannel(notificationChannel);
+        nm.notify(0, builder.build());
+    }
+
     @Override
     public void onReceive(Context context, Intent intent) {
         if (ACTION_INSTALL_REBOOT.equals(intent.getAction())) {
@@ -37,6 +92,11 @@
         } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
             SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
             pref.edit().remove(Constants.PREF_NEEDS_REBOOT).apply();
+
+            if (shouldShowUpdateFailedNotification(context)) {
+                pref.edit().putBoolean(Constants.PREF_INSTALL_NOTIFIED, true).apply();
+                showUpdateFailedNotification(context);
+            }
         }
     }
 }
diff --git a/src/org/lineageos/updater/controller/UpdateInstaller.java b/src/org/lineageos/updater/controller/UpdateInstaller.java
index aaae06b..ccac1c9 100644
--- a/src/org/lineageos/updater/controller/UpdateInstaller.java
+++ b/src/org/lineageos/updater/controller/UpdateInstaller.java
@@ -16,7 +16,9 @@
 package org.lineageos.updater.controller;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.support.v7.preference.PreferenceManager;
 import android.util.Log;
 
@@ -55,15 +57,20 @@
 
     void install(String downloadId) {
         UpdateInfo update = mUpdaterController.getUpdate(downloadId);
-        boolean deleteUpdate = PreferenceManager.getDefaultSharedPreferences(mContext)
-                .getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false);
-        if (deleteUpdate) {
-            // Renaming the file is enough to have it deleted automatically
-            File uncrytpFile = new File(
-                    update.getFile().getAbsolutePath() + Constants.UNCRYPT_FILE_EXT);
-            update.getFile().renameTo(uncrytpFile);
-            installPackage(uncrytpFile, downloadId);
-        } else if (Utils.isEncrypted(mContext, update.getFile())) {
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+        long buildTimestamp = SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0);
+        long lastBuildTimestamp = preferences.getLong(Constants.PREF_INSTALL_OLD_TIMESTAMP,
+                buildTimestamp);
+        boolean isReinstalling = buildTimestamp == lastBuildTimestamp;
+        preferences.edit()
+                .putLong(Constants.PREF_INSTALL_OLD_TIMESTAMP, buildTimestamp)
+                .putLong(Constants.PREF_INSTALL_NEW_TIMESTAMP, update.getTimestamp())
+                .putString(Constants.PREF_INSTALL_PACKAGE_PATH, update.getFile().getAbsolutePath())
+                .putBoolean(Constants.PREF_INSTALL_AGAIN, isReinstalling)
+                .putBoolean(Constants.PREF_INSTALL_NOTIFIED, false)
+                .apply();
+
+        if (Utils.isEncrypted(mContext, update.getFile())) {
             // uncrypt rewrites the file so that it can be read without mounting
             // the filesystem, so create a copy of it.
             prepareForUncryptAndInstall(update);
diff --git a/src/org/lineageos/updater/misc/Constants.java b/src/org/lineageos/updater/misc/Constants.java
index b5764ef..49106d4 100644
--- a/src/org/lineageos/updater/misc/Constants.java
+++ b/src/org/lineageos/updater/misc/Constants.java
@@ -39,4 +39,9 @@
     public static final String PROP_RELEASE_TYPE = "ro.lineage.releasetype";
     public static final String PROP_UPDATER_URI = "lineage.updater.uri";
 
+    public static final String PREF_INSTALL_OLD_TIMESTAMP = "install_old_timestamp";
+    public static final String PREF_INSTALL_NEW_TIMESTAMP = "install_new_timestamp";
+    public static final String PREF_INSTALL_PACKAGE_PATH = "install_package_path";
+    public static final String PREF_INSTALL_AGAIN = "install_again";
+    public static final String PREF_INSTALL_NOTIFIED = "install_notified";
 }
diff --git a/src/org/lineageos/updater/misc/Utils.java b/src/org/lineageos/updater/misc/Utils.java
index 255b750..67895da 100644
--- a/src/org/lineageos/updater/misc/Utils.java
+++ b/src/org/lineageos/updater/misc/Utils.java
@@ -264,11 +264,26 @@
      */
     public static void cleanupDownloadsDir(Context context) {
         File downloadPath = getDownloadPath(context);
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
 
         removeUncryptFiles(downloadPath);
 
+        long buildTimestamp = SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0);
+        long prevTimestamp = preferences.getLong(Constants.PREF_INSTALL_OLD_TIMESTAMP, 0);
+        String lastUpdatePath = preferences.getString(Constants.PREF_INSTALL_PACKAGE_PATH, null);
+        boolean reinstalling = preferences.getBoolean(Constants.PREF_INSTALL_AGAIN, false);
+        boolean deleteUpdates = preferences.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false);
+        if ((buildTimestamp != prevTimestamp || reinstalling) && deleteUpdates &&
+                lastUpdatePath != null) {
+            File lastUpdate = new File(lastUpdatePath);
+            if (lastUpdate.exists()) {
+                lastUpdate.delete();
+                // Remove the pref not to delete the file if re-downloaded
+                preferences.edit().remove(Constants.PREF_INSTALL_PACKAGE_PATH).apply();
+            }
+        }
+
         final String DOWNLOADS_CLEANUP_DONE = "cleanup_done";
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
         if (preferences.getBoolean(DOWNLOADS_CLEANUP_DONE, false)) {
             return;
         }