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

This commit is contained in:
Daniel Gultsch 2019-01-23 11:20:36 +01:00
parent c9fc40dfe5
commit 18982174ce
6 changed files with 237 additions and 157 deletions

View file

@ -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) {
@ -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();
}
}

View file

@ -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:
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) {

View file

@ -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) {
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());
}

View file

@ -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);
}
}
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))
@ -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;

View file

@ -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) {

View file

@ -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;
}
}