create share button in backup done notification

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/services/ExportBackupService.java   | 134 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java |   1 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java        |   3 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                |   2 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                 |   5 
src/main/res/drawable-hdpi/ic_backup_black_48dp.png                      |   0 
src/main/res/drawable-hdpi/ic_backup_white_48dp.png                      |   0 
src/main/res/drawable-mdpi/ic_backup_black_48dp.png                      |   0 
src/main/res/drawable-mdpi/ic_backup_white_48dp.png                      |   0 
src/main/res/drawable-xhdpi/ic_backup_black_48dp.png                     |   0 
src/main/res/drawable-xhdpi/ic_backup_white_48dp.png                     |   0 
src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png                    |   0 
src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png                    |   0 
src/main/res/drawable-xxxhdpi/ic_backup_black_48dp.png                   |   0 
src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png                   |   0 
src/main/res/values/attrs.xml                                            |   1 
src/main/res/values/strings.xml                                          |   3 
src/main/res/values/themes.xml                                           |   2 
18 files changed, 98 insertions(+), 53 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/ExportBackupService.java 🔗

@@ -21,7 +21,9 @@ import java.io.PrintWriter;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPOutputStream;
@@ -50,6 +52,8 @@ public class ExportBackupService extends Service {
     public static final String CIPHERMODE = "AES/GCM/NoPadding";
     public static final String PROVIDER = "BC";
 
+    public static final String MIME_TYPE = "application/vnd.conversations.backup";
+
     private static final int NOTIFICATION_ID = 19;
     private static final int PAGE_SIZE = 20;
     private static AtomicBoolean running = new AtomicBoolean(false);
@@ -213,11 +217,19 @@ public class ExportBackupService extends Service {
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (running.compareAndSet(false, true)) {
             new Thread(() -> {
-                final boolean success = export();
+                boolean success;
+                List<File> files;
+                try {
+                    files = export();
+                    success = true;
+                } catch (Exception e) {
+                    success = false;
+                    files = Collections.emptyList();
+                }
                 stopForeground(true);
                 running.set(false);
                 if (success) {
-                    notifySuccess();
+                    notifySuccess(files);
                 }
                 stopSelf();
             }).start();
@@ -250,81 +262,97 @@ public class ExportBackupService extends Service {
         }
     }
 
-    private boolean export() {
+    private List<File> export() throws Exception {
         NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
                 .setSmallIcon(R.drawable.ic_archive_white_24dp)
                 .setProgress(1, 0, false);
         startForeground(NOTIFICATION_ID, mBuilder.build());
-        try {
-            int count = 0;
-            final int max = this.mAccounts.size();
-            final SecureRandom secureRandom = new SecureRandom();
-            for (Account account : this.mAccounts) {
-                final byte[] IV = new byte[12];
-                final byte[] salt = new byte[16];
-                secureRandom.nextBytes(IV);
-                secureRandom.nextBytes(salt);
-                final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
-                final Progress progress = new Progress(mBuilder, max, count);
-                final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
-                if (file.getParentFile().mkdirs()) {
-                    Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
-                }
-                final FileOutputStream fileOutputStream = new FileOutputStream(file);
-                final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
-                backupFileHeader.write(dataOutputStream);
-                dataOutputStream.flush();
-
-                final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
-                byte[] key = getKey(account.getPassword(), salt);
-                Log.d(Config.LOGTAG, backupFileHeader.toString());
-                SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
-                IvParameterSpec ivSpec = new IvParameterSpec(IV);
-                cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
-                CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
-
-                GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
-                PrintWriter writer = new PrintWriter(gzipOutputStream);
-                SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
-                final String uuid = account.getUuid();
-                accountExport(db, uuid, writer);
-                simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
-                messageExport(db, uuid, writer, progress);
-                for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
-                    simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
-                }
-                writer.flush();
-                writer.close();
-                Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
-                count++;
+        int count = 0;
+        final int max = this.mAccounts.size();
+        final SecureRandom secureRandom = new SecureRandom();
+        final List<File> files = new ArrayList<>();
+        for (Account account : this.mAccounts) {
+            final byte[] IV = new byte[12];
+            final byte[] salt = new byte[16];
+            secureRandom.nextBytes(IV);
+            secureRandom.nextBytes(salt);
+            final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
+            final Progress progress = new Progress(mBuilder, max, count);
+            final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
+            files.add(file);
+            if (file.getParentFile().mkdirs()) {
+                Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
+            }
+            final FileOutputStream fileOutputStream = new FileOutputStream(file);
+            final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
+            backupFileHeader.write(dataOutputStream);
+            dataOutputStream.flush();
+
+            final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+            byte[] key = getKey(account.getPassword(), salt);
+            Log.d(Config.LOGTAG, backupFileHeader.toString());
+            SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+            IvParameterSpec ivSpec = new IvParameterSpec(IV);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+            CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
+
+            GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
+            PrintWriter writer = new PrintWriter(gzipOutputStream);
+            SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
+            final String uuid = account.getUuid();
+            accountExport(db, uuid, writer);
+            simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
+            messageExport(db, uuid, writer, progress);
+            for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
+                simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
             }
-            return true;
-        } catch (Exception e) {
-            Log.d(Config.LOGTAG, "unable to create backup ", e);
-            return false;
+            writer.flush();
+            writer.close();
+            Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
+            count++;
         }
+        return files;
     }
 
-    private void notifySuccess() {
+    private void notifySuccess(List<File> files) {
         final String path = FileBackend.getBackupDirectory(this);
 
-        PendingIntent pendingIntent = null;
+        PendingIntent openFolderIntent = null;
 
         for (Intent intent : getPossibleFileOpenIntents(this, path)) {
             if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
-                pendingIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+                openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                 break;
             }
         }
 
+        PendingIntent shareFilesIntent = null;
+        if (files.size() > 0) {
+            final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+            ArrayList<Uri> uris = new ArrayList<>();
+            for(File file : files) {
+                uris.add(FileBackend.getUriForFile(this, file));
+            }
+            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.setType(MIME_TYPE);
+            final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
+            shareFilesIntent = PendingIntent.getActivity(this,190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
+        }
+
         NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.notification_backup_created_title))
                 .setContentText(getString(R.string.notification_backup_created_subtitle, path))
                 .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this))))
                 .setAutoCancel(true)
-                .setContentIntent(pendingIntent)
+                .setContentIntent(openFolderIntent)
                 .setSmallIcon(R.drawable.ic_archive_white_24dp);
+
+        if (shareFilesIntent != null) {
+            mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
+        }
+
         notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
     }
 

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java 🔗

@@ -94,6 +94,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
                     imageResource = activity.getThemeResource(R.attr.ic_attach_location, R.drawable.ic_attach_location);
                     showPreviewText = false;
                 } else {
+                    //TODO move this into static MediaPreview method and use same icons as in MediaAdapter
                     final String mime = message.getMimeType();
                     switch (mime == null ? "" : mime.split("/")[0]) {
                         case "image":

src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java 🔗

@@ -23,6 +23,7 @@ import java.util.concurrent.RejectedExecutionException;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.MediaBinding;
+import eu.siacs.conversations.services.ExportBackupService;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.ui.util.StyledAttributes;
@@ -79,6 +80,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
                 attr = R.attr.media_preview_archive;
             } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
                 attr = R.attr.media_preview_ebook;
+            } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+                attr = R.attr.media_preview_backup;
             } else if (DOCUMENT_MIMES.contains(mime)) {
                 attr = R.attr.media_preview_document;
             } else {

src/main/java/eu/siacs/conversations/utils/MimeUtils.java 🔗

@@ -29,6 +29,7 @@ import java.util.Properties;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.ExportBackupService;
 
 /**
  * Utilities for dealing with MIME types.
@@ -72,6 +73,7 @@ public final class MimeUtils {
         add("application/vnd.amazon.mobi8-ebook","kfx");
         add("application/vnd.android.package-archive", "apk");
         add("application/vnd.cinderella", "cdy");
+        add(ExportBackupService.MIME_TYPE, "ceb");
         add("application/vnd.ms-pki.stl", "stl");
         add("application/vnd.oasis.opendocument.database", "odb");
         add("application/vnd.oasis.opendocument.formula", "odf");

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -32,6 +32,7 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.ExportBackupService;
 import rocks.xmpp.addr.Jid;
 
 public class UIHelper {
@@ -483,8 +484,12 @@ public class UIHelper {
 			return context.getString(R.string.pdf_document);
 		} else if (mime.equals("application/vnd.android.package-archive")) {
 			return context.getString(R.string.apk);
+		} else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+			return context.getString(R.string.conversations_backup);
 		} else if (mime.contains("vcard")) {
 			return context.getString(R.string.vcard);
+		} else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) {
+			return context.getString(R.string.event);
 		} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
 			return context.getString(R.string.ebook);
 		} else {

src/main/res/values/attrs.xml 🔗

@@ -63,6 +63,7 @@
     <attr name="media_preview_calendar" format="reference"/>
     <attr name="media_preview_archive" format="reference" />
     <attr name="media_preview_ebook" format="reference"/>
+    <attr name="media_preview_backup" format="reference"/>
     <attr name="media_preview_unknown" format="reference" />
 
 

src/main/res/values/strings.xml 🔗

@@ -868,4 +868,7 @@
     <string name="this_looks_like_a_domain">This looks like a domain address</string>
     <string name="add_anway">Add anyway</string>
     <string name="this_looks_like_channel">This looks like a channel address</string>
+    <string name="share_backup_files">Share backup files</string>
+    <string name="conversations_backup">Conversations backup</string>
+    <string name="event">Event</string>
 </resources>

src/main/res/values/themes.xml 🔗

@@ -74,6 +74,7 @@
         <item type="reference" name="media_preview_calendar">@drawable/ic_event_black_48dp</item>
         <item type="reference" name="media_preview_archive">@drawable/ic_archive_black_48dp</item>
         <item type="reference" name="media_preview_ebook">@drawable/ic_book_black_48dp</item>
+        <item type="reference" name="media_preview_backup">@drawable/ic_backup_black_48dp</item>
         <item type="reference" name="media_preview_unknown">@drawable/ic_help_black_48dp</item>
 
         <item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>
@@ -187,6 +188,7 @@
         <item type="reference" name="media_preview_calendar">@drawable/ic_event_white_48dp</item>
         <item type="reference" name="media_preview_archive">@drawable/ic_archive_white_48dp</item>
         <item type="reference" name="media_preview_ebook">@drawable/ic_book_white_48dp</item>
+        <item type="reference" name="media_preview_backup">@drawable/ic_backup_white_48dp</item>
         <item type="reference" name="media_preview_unknown">@drawable/ic_help_white_48dp</item>
 
         <item type="reference" name="icon_add_group">@drawable/ic_group_add_white_24dp</item>