ask for permissions before opening restore backup. use insert or ignore for messages

Daniel Gultsch created

Change summary

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java |  45 
src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java     |  31 
src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java           |  34 
src/main/java/eu/siacs/conversations/services/ExportBackupService.java          | 202 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java               |  30 
src/main/java/eu/siacs/conversations/utils/PermissionUtils.java                 |  34 
6 files changed, 228 insertions(+), 148 deletions(-)

Detailed changes

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java 🔗

@@ -5,6 +5,7 @@ import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.Binder;
 import android.os.IBinder;
@@ -20,13 +21,13 @@ import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPInputStream;
 
-import javax.crypto.AEADBadTagException;
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.CipherInputStream;
@@ -41,6 +42,7 @@ import eu.siacs.conversations.ui.ManageAccountActivity;
 import eu.siacs.conversations.utils.BackupFileHeader;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import rocks.xmpp.addr.Jid;
 
 import static eu.siacs.conversations.services.ExportBackupService.CIPHERMODE;
 import static eu.siacs.conversations.services.ExportBackupService.KEYTYPE;
@@ -49,13 +51,10 @@ import static eu.siacs.conversations.services.ExportBackupService.PROVIDER;
 public class ImportBackupService extends Service {
 
     private static final int NOTIFICATION_ID = 21;
-
+    private static AtomicBoolean running = new AtomicBoolean(false);
     private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
     private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
-
     private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
-
-    private static AtomicBoolean running = new AtomicBoolean(false);
     private DatabaseBackend mDatabaseBackend;
     private NotificationManager notificationManager;
 
@@ -85,7 +84,6 @@ public class ImportBackupService extends Service {
         if (password == null || file == null) {
             return START_NOT_STICKY;
         }
-        Log.d(Config.LOGTAG, "on start command");
         if (running.compareAndSet(false, true)) {
             executor.execute(() -> {
                 startForegroundService();
@@ -106,7 +104,8 @@ public class ImportBackupService extends Service {
     public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
         executor.execute(() -> {
             final ArrayList<BackupFile> backupFiles = new ArrayList<>();
-            for (String app : Arrays.asList("Conversations", "Quicksy")) {
+            final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
+            for (String app : apps) {
                 final File directory = new File(FileBackend.getBackupDirectory(app));
                 if (!directory.exists() || !directory.isDirectory()) {
                     Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
@@ -154,9 +153,11 @@ public class ImportBackupService extends Service {
             BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
             String line;
             StringBuilder multiLineQuery = null;
+            int error = 0;
             while ((line = reader.readLine()) != null) {
                 int count = count(line, '\'');
                 if (multiLineQuery != null) {
+                    multiLineQuery.append('\n');
                     multiLineQuery.append(line);
                     if (count % 2 == 1) {
                         db.execSQL(multiLineQuery.toString());
@@ -171,6 +172,12 @@ public class ImportBackupService extends Service {
                 }
             }
             Log.d(Config.LOGTAG, "done reading file");
+            final Jid jid = backupFileHeader.getJid();
+            Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain()});
+            countCursor.moveToFirst();
+            int count = countCursor.getInt(0);
+            Log.d(Config.LOGTAG, "restored " + count + " messages");
+            countCursor.close();
             stopBackgroundService();
             synchronized (mOnBackupProcessedListeners) {
                 for (OnBackupProcessed l : mOnBackupProcessedListeners) {
@@ -207,7 +214,7 @@ public class ImportBackupService extends Service {
                 .setAutoCancel(true)
                 .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
                 .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
-        notificationManager.notify(NOTIFICATION_ID,mBuilder.build());
+        notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
     }
 
     private void stopBackgroundService() {
@@ -232,6 +239,18 @@ public class ImportBackupService extends Service {
         return this.binder;
     }
 
+    public interface OnBackupFilesLoaded {
+        void onBackupFilesLoaded(List<BackupFile> files);
+    }
+
+    public interface OnBackupProcessed {
+        void onBackupRestored();
+
+        void onBackupDecryptionFailed();
+
+        void onBackupRestoreFailed();
+    }
+
     public static class BackupFile {
         private final File file;
         private final BackupFileHeader header;
@@ -263,14 +282,4 @@ public class ImportBackupService extends Service {
             return ImportBackupService.this;
         }
     }
-
-    public interface OnBackupFilesLoaded {
-        void onBackupFilesLoaded(List<BackupFile> files);
-    }
-
-    public interface OnBackupProcessed {
-        void onBackupRestored();
-        void onBackupDecryptionFailed();
-        void onBackupRestoreFailed();
-    }
 }

src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java 🔗

@@ -5,6 +5,7 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.security.KeyChain;
 import android.security.KeyChainAliasCallback;
+import android.support.annotation.NonNull;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
 import android.util.Pair;
@@ -35,10 +36,15 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import rocks.xmpp.addr.Jid;
 
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
 public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
 
     private final String STATE_SELECTED_ACCOUNT = "selected_account";
 
+    private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
+
     protected Account selectedAccount = null;
     protected Jid selectedAccountJid = null;
 
@@ -201,7 +207,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
                 startActivity(new Intent(this, EditAccountActivity.class));
                 break;
             case R.id.action_import_backup:
-                startActivity(new Intent(this, ImportBackupActivity.class));
+                if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+                    startActivity(new Intent(this, ImportBackupActivity.class));
+                }
                 break;
             case R.id.action_disable_all:
                 disableAllAccounts();
@@ -218,6 +226,27 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
         return super.onOptionsItemSelected(item);
     }
 
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+        if (grantResults.length > 0) {
+            if (allGranted(grantResults)) {
+                switch (requestCode) {
+                    case REQUEST_IMPORT_BACKUP:
+                        startActivity(new Intent(this, ImportBackupActivity.class));
+                        break;
+                }
+            } else {
+                Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+            }
+        }
+        if (writeGranted(grantResults, permissions)) {
+            if (xmppConnectionService != null) {
+                xmppConnectionService.restartFileObserver();
+            }
+        }
+    }
+
     @Override
     public boolean onNavigateUp() {
         if (xmppConnectionService.getConversations().size() == 0) {

src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java 🔗

@@ -3,22 +3,26 @@ package eu.siacs.conversations.ui;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.Bundle;
-import android.support.v4.content.ContextCompat;
+import android.support.annotation.NonNull;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.widget.Button;
+import android.widget.Toast;
 
 import java.util.List;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.ImportBackupService;
-import eu.siacs.conversations.utils.XmppUri;
+
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 
 public class WelcomeActivity extends XmppActivity {
 
+    private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
+
     @Override
     protected void refreshUiReal() {
 
@@ -90,12 +94,34 @@ public class WelcomeActivity extends XmppActivity {
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == R.id.action_import_backup) {
-            startActivity(new Intent(this, ImportBackupActivity.class));
+            if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+                startActivity(new Intent(this, ImportBackupActivity.class));
+            }
             return true;
         }
         return super.onOptionsItemSelected(item);
     }
 
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+        if (grantResults.length > 0) {
+            if (allGranted(grantResults)) {
+                switch (requestCode) {
+                    case REQUEST_IMPORT_BACKUP:
+                        startActivity(new Intent(this, ImportBackupActivity.class));
+                        break;
+                }
+            } else {
+                Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+            }
+        }
+        if (writeGranted(grantResults, permissions)) {
+            if (xmppConnectionService != null) {
+                xmppConnectionService.restartFileObserver();
+            }
+        }
+    }
+
     public void addInviteUri(Intent intent) {
         StartConversationActivity.addInviteUri(intent, getIntent());
     }

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

@@ -49,33 +49,14 @@ public class ExportBackupService extends Service {
     public static final String PROVIDER = "BC";
 
     private static final int NOTIFICATION_ID = 19;
+    private static final int PAGE_SIZE = 20;
     private static AtomicBoolean running = new AtomicBoolean(false);
     private DatabaseBackend mDatabaseBackend;
     private List<Account> mAccounts;
     private NotificationManager notificationManager;
 
-    @Override
-    public void onCreate() {
-        mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
-        mAccounts = mDatabaseBackend.getAccounts();
-        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (running.compareAndSet(false, true)) {
-            new Thread(() -> {
-                export();
-                stopForeground(true);
-                running.set(false);
-                stopSelf();
-            }).start();
-        }
-        return START_NOT_STICKY;
-    }
-
     private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
-        StringBuilder builder = new StringBuilder();
+        final StringBuilder builder = new StringBuilder();
         final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
         while (accountCursor != null && accountCursor.moveToNext()) {
             builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
@@ -95,10 +76,8 @@ public class ExportBackupService extends Service {
                     builder.append("NULL");
                 } else if (value.matches("\\d+")) {
                     int intValue = Integer.parseInt(value);
-                    Log.d(Config.LOGTAG,"reading int value. "+intValue);
                     if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
                         intValue |= 1 << Account.OPTION_DISABLED;
-                        Log.d(Config.LOGTAG,"modified int value "+intValue);
                     }
                     builder.append(intValue);
                 } else {
@@ -109,13 +88,103 @@ public class ExportBackupService extends Service {
             builder.append(';');
             builder.append('\n');
         }
-        Log.d(Config.LOGTAG,builder.toString());
         if (accountCursor != null) {
             accountCursor.close();
         }
         writer.append(builder.toString());
     }
 
+    private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
+        final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
+        while (cursor != null && cursor.moveToNext()) {
+            writer.write(cursorToString(table, cursor, PAGE_SIZE));
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+    }
+
+    public static byte[] getKey(String password, byte[] salt) {
+        try {
+            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+            return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
+        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private static String cursorToString(String tablename, Cursor cursor, int max) {
+        return cursorToString(tablename, cursor, max, false);
+    }
+
+    private static String cursorToString(String tablename, Cursor cursor, int max, boolean ignore) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("INSERT ");
+        if (ignore) {
+            builder.append("OR IGNORE ");
+        }
+        builder.append("INTO ").append(tablename).append("(");
+        for (int i = 0; i < cursor.getColumnCount(); ++i) {
+            if (i != 0) {
+                builder.append(',');
+            }
+            builder.append(cursor.getColumnName(i));
+        }
+        builder.append(") VALUES");
+        for (int i = 0; i < max; ++i) {
+            if (i != 0) {
+                builder.append(',');
+            }
+            appendValues(cursor, builder);
+            if (i < max - 1 && !cursor.moveToNext()) {
+                break;
+            }
+        }
+        builder.append(';');
+        builder.append('\n');
+        return builder.toString();
+    }
+
+    private static void appendValues(Cursor cursor, StringBuilder builder) {
+        builder.append("(");
+        for (int i = 0; i < cursor.getColumnCount(); ++i) {
+            if (i != 0) {
+                builder.append(',');
+            }
+            final String value = cursor.getString(i);
+            if (value == null) {
+                builder.append("NULL");
+            } else if (value.matches("\\d+")) {
+                builder.append(value);
+            } else {
+                DatabaseUtils.appendEscapedSQLString(builder, value);
+            }
+        }
+        builder.append(")");
+
+    }
+
+    @Override
+    public void onCreate() {
+        mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+        mAccounts = mDatabaseBackend.getAccounts();
+        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (running.compareAndSet(false, true)) {
+            new Thread(() -> {
+                export();
+                stopForeground(true);
+                running.set(false);
+                stopSelf();
+            }).start();
+            return START_STICKY;
+        }
+        return START_NOT_STICKY;
+    }
+
     private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
         Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
         int size = cursor != null ? cursor.getCount() : 0;
@@ -123,17 +192,16 @@ public class ExportBackupService extends Service {
         int i = 0;
         int p = 0;
         while (cursor != null && cursor.moveToNext()) {
-            writer.write(cursorToString(Message.TABLENAME, cursor, 20));
-            if (i + 20 > size) {
+            writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
+            if (i + PAGE_SIZE > size) {
                 i = size;
             } else {
-                i += 20;
+                i += PAGE_SIZE;
             }
             final int percentage = i * 100 / size;
             if (p < percentage) {
                 p = percentage;
-                notificationManager.notify(NOTIFICATION_ID,progress.build(p));
-                Log.d(Config.LOGTAG, "percentage=" + p);
+                notificationManager.notify(NOTIFICATION_ID, progress.build(p));
             }
         }
         if (cursor != null) {
@@ -141,16 +209,6 @@ public class ExportBackupService extends Service {
         }
     }
 
-    private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
-        final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
-        while (cursor != null && cursor.moveToNext()) {
-            writer.write(cursorToString(table, cursor, 20));
-        }
-        if (cursor != null) {
-            cursor.close();
-        }
-    }
-
     private void export() {
         NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
@@ -166,11 +224,11 @@ public class ExportBackupService extends Service {
                 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 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");
+                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());
+                    Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
                 }
                 final FileOutputStream fileOutputStream = new FileOutputStream(file);
                 final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
@@ -179,7 +237,7 @@ public class ExportBackupService extends Service {
 
                 final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
                 byte[] key = getKey(account.getPassword(), salt);
-                Log.d(Config.LOGTAG,backupFileHeader.toString());
+                Log.d(Config.LOGTAG, backupFileHeader.toString());
                 SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
                 IvParameterSpec ivSpec = new IvParameterSpec(IV);
                 cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
@@ -192,8 +250,8 @@ public class ExportBackupService extends Service {
                 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);
+                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();
@@ -205,58 +263,6 @@ public class ExportBackupService extends Service {
         }
     }
 
-    public static byte[] getKey(String password, byte[] salt) {
-        try {
-            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
-            return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
-        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
-            throw new AssertionError(e);
-        }
-    }
-
-    private static String cursorToString(String tablename, Cursor cursor, int max) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("INSERT INTO ").append(tablename).append("(");
-        for (int i = 0; i < cursor.getColumnCount(); ++i) {
-            if (i != 0) {
-                builder.append(',');
-            }
-            builder.append(cursor.getColumnName(i));
-        }
-        builder.append(") VALUES");
-        for (int i = 0; i < max; ++i) {
-            if (i != 0) {
-                builder.append(',');
-            }
-            appendValues(cursor, builder);
-            if (!cursor.moveToNext()) {
-                break;
-            }
-        }
-        builder.append(';');
-        builder.append('\n');
-        return builder.toString();
-    }
-
-    private static void appendValues(Cursor cursor, StringBuilder builder) {
-        builder.append("(");
-        for (int i = 0; i < cursor.getColumnCount(); ++i) {
-            if (i != 0) {
-                builder.append(',');
-            }
-            final String value = cursor.getString(i);
-            if (value == null) {
-                builder.append("NULL");
-            } else if (value.matches("\\d+")) {
-                builder.append(value);
-            } else {
-                DatabaseUtils.appendEscapedSQLString(builder, value);
-            }
-        }
-        builder.append(")");
-
-    }
-
     @Override
     public IBinder onBind(Intent intent) {
         return null;
@@ -274,7 +280,7 @@ public class ExportBackupService extends Service {
         }
 
         private Notification build(int percentage) {
-            builder.setProgress(max * 100,count * 100 + percentage,false);
+            builder.setProgress(max * 100, count * 100 + percentage, false);
             return builder.build();
         }
     }

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -119,6 +119,9 @@ import rocks.xmpp.addr.Jid;
 import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
 import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
 import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 
 
 public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
@@ -523,33 +526,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         return getConversation(activity, R.id.main_fragment);
     }
 
-    private static boolean allGranted(int[] grantResults) {
-        for (int grantResult : grantResults) {
-            if (grantResult != PackageManager.PERMISSION_GRANTED) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static boolean writeGranted(int[] grantResults, String[] permission) {
-        for (int i = 0; i < grantResults.length; ++i) {
-            if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
-                return grantResults[i] == PackageManager.PERMISSION_GRANTED;
-            }
-        }
-        return false;
-    }
-
-    private static String getFirstDenied(int[] grantResults, String[] permissions) {
-        for (int i = 0; i < grantResults.length; ++i) {
-            if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
-                return permissions[i];
-            }
-        }
-        return null;
-    }
-
     private static boolean scrolledToBottom(AbsListView listView) {
         final int count = listView.getCount();
         if (count == 0) {

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

@@ -0,0 +1,34 @@
+package eu.siacs.conversations.utils;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+
+public class PermissionUtils {
+
+    public static boolean allGranted(int[] grantResults) {
+        for (int grantResult : grantResults) {
+            if (grantResult != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean writeGranted(int[] grantResults, String[] permission) {
+        for (int i = 0; i < grantResults.length; ++i) {
+            if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
+                return grantResults[i] == PackageManager.PERMISSION_GRANTED;
+            }
+        }
+        return false;
+    }
+
+    public static String getFirstDenied(int[] grantResults, String[] permissions) {
+        for (int i = 0; i < grantResults.length; ++i) {
+            if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
+                return permissions[i];
+            }
+        }
+        return null;
+    }
+}