allow backup to be restored from selected file
This commit is contained in:
		
							parent
							
								
									b68851b719
								
							
						
					
					
						commit
						603e1b35a5
					
				|  | @ -7,6 +7,7 @@ import android.content.Context; | |||
| import android.content.Intent; | ||||
| import android.database.Cursor; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.net.Uri; | ||||
| import android.os.Binder; | ||||
| import android.os.IBinder; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
|  | @ -16,18 +17,20 @@ import java.io.BufferedReader; | |||
| import java.io.DataInputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.InputStreamReader; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| 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 java.util.zip.ZipException; | ||||
| 
 | ||||
| import javax.crypto.BadPaddingException; | ||||
| import javax.crypto.Cipher; | ||||
|  | @ -81,14 +84,22 @@ public class ImportBackupService extends Service { | |||
|             return START_NOT_STICKY; | ||||
|         } | ||||
|         final String password = intent.getStringExtra("password"); | ||||
|         final String file = intent.getStringExtra("file"); | ||||
|         if (password == null || file == null) { | ||||
|         final Uri data = intent.getData(); | ||||
|         final Uri uri; | ||||
|         if (data == null) { | ||||
|             final String file = intent.getStringExtra("file"); | ||||
|             uri = file == null ? null : Uri.fromFile(new File(file)); | ||||
|         } else { | ||||
|             uri = data; | ||||
|         } | ||||
| 
 | ||||
|         if (password == null || uri == null) { | ||||
|             return START_NOT_STICKY; | ||||
|         } | ||||
|         if (running.compareAndSet(false, true)) { | ||||
|             executor.execute(() -> { | ||||
|                 startForegroundService(); | ||||
|                 final boolean success = importBackup(new File(file), password); | ||||
|                 final boolean success = importBackup(uri, password); | ||||
|                 stopForeground(true); | ||||
|                 running.set(false); | ||||
|                 if (success) { | ||||
|  | @ -122,7 +133,7 @@ public class ImportBackupService extends Service { | |||
|                         try { | ||||
|                             final BackupFile backupFile = BackupFile.read(file); | ||||
|                             if (accounts.contains(backupFile.getHeader().getJid())) { | ||||
|                                 Log.d(Config.LOGTAG,"skipping backup for "+backupFile.getHeader().getJid()); | ||||
|                                 Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid()); | ||||
|                             } else { | ||||
|                                 backupFiles.add(backupFile); | ||||
|                             } | ||||
|  | @ -145,21 +156,43 @@ public class ImportBackupService extends Service { | |||
|         startForeground(NOTIFICATION_ID, mBuilder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private boolean importBackup(File file, String password) { | ||||
|         Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath()); | ||||
|     private boolean importBackup(Uri uri, String password) { | ||||
|         Log.d(Config.LOGTAG, "importing backup from " + uri); | ||||
|         if (password == null || password.isEmpty()) { | ||||
|             synchronized (mOnBackupProcessedListeners) { | ||||
|                 for (OnBackupProcessed l : mOnBackupProcessedListeners) { | ||||
|                     l.onBackupDecryptionFailed(); | ||||
|                 } | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); | ||||
|             final FileInputStream fileInputStream = new FileInputStream(file); | ||||
|             final DataInputStream dataInputStream = new DataInputStream(fileInputStream); | ||||
|             BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); | ||||
|             final InputStream inputStream; | ||||
|             if ("file".equals(uri.getScheme())) { | ||||
|                 inputStream = new FileInputStream(new File(uri.getPath())); | ||||
|             } else { | ||||
|                 inputStream = getContentResolver().openInputStream(uri); | ||||
|             } | ||||
|             final DataInputStream dataInputStream = new DataInputStream(inputStream); | ||||
|             final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); | ||||
|             Log.d(Config.LOGTAG, backupFileHeader.toString()); | ||||
| 
 | ||||
|             if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { | ||||
|                 synchronized (mOnBackupProcessedListeners) { | ||||
|                     for (OnBackupProcessed l : mOnBackupProcessedListeners) { | ||||
|                         l.onAccountAlreadySetup(); | ||||
|                     } | ||||
|                 } | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); | ||||
|             byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt()); | ||||
|             SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); | ||||
|             IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv()); | ||||
|             cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); | ||||
|             CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher); | ||||
|             CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); | ||||
| 
 | ||||
|             GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); | ||||
|             BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8")); | ||||
|  | @ -197,12 +230,7 @@ public class ImportBackupService extends Service { | |||
|             return true; | ||||
|         } catch (Exception e) { | ||||
|             Throwable throwable = e.getCause(); | ||||
|             final boolean reasonWasCrypto; | ||||
|             if (throwable instanceof BadPaddingException) { | ||||
|                 reasonWasCrypto = true; | ||||
|             } else { | ||||
|                 reasonWasCrypto = false; | ||||
|             } | ||||
|             final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException; | ||||
|             synchronized (mOnBackupProcessedListeners) { | ||||
|                 for (OnBackupProcessed l : mOnBackupProcessedListeners) { | ||||
|                     if (reasonWasCrypto) { | ||||
|  | @ -212,7 +240,7 @@ public class ImportBackupService extends Service { | |||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e); | ||||
|             Log.d(Config.LOGTAG, "error restoring backup " + uri, e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | @ -259,14 +287,16 @@ public class ImportBackupService extends Service { | |||
|         void onBackupDecryptionFailed(); | ||||
| 
 | ||||
|         void onBackupRestoreFailed(); | ||||
| 
 | ||||
|         void onAccountAlreadySetup(); | ||||
|     } | ||||
| 
 | ||||
|     public static class BackupFile { | ||||
|         private final File file; | ||||
|         private final Uri uri; | ||||
|         private final BackupFileHeader header; | ||||
| 
 | ||||
|         private BackupFile(File file, BackupFileHeader header) { | ||||
|             this.file = file; | ||||
|         private BackupFile(Uri uri, BackupFileHeader header) { | ||||
|             this.uri = uri; | ||||
|             this.header = header; | ||||
|         } | ||||
| 
 | ||||
|  | @ -275,15 +305,26 @@ public class ImportBackupService extends Service { | |||
|             final DataInputStream dataInputStream = new DataInputStream(fileInputStream); | ||||
|             BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); | ||||
|             fileInputStream.close(); | ||||
|             return new BackupFile(file, backupFileHeader); | ||||
|             return new BackupFile(Uri.fromFile(file), backupFileHeader); | ||||
|         } | ||||
| 
 | ||||
|         public static BackupFile read(final Context context, final Uri uri) throws IOException { | ||||
|             final InputStream inputStream = context.getContentResolver().openInputStream(uri); | ||||
|             if (inputStream == null) { | ||||
|                 throw new FileNotFoundException(); | ||||
|             } | ||||
|             final DataInputStream dataInputStream = new DataInputStream(inputStream); | ||||
|             BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); | ||||
|             inputStream.close(); | ||||
|             return new BackupFile(uri, backupFileHeader); | ||||
|         } | ||||
| 
 | ||||
|         public BackupFileHeader getHeader() { | ||||
|             return header; | ||||
|         } | ||||
| 
 | ||||
|         public File getFile() { | ||||
|             return file; | ||||
|         public Uri getUri() { | ||||
|             return uri; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ import android.content.Context; | |||
| import android.content.Intent; | ||||
| import android.content.ServiceConnection; | ||||
| import android.databinding.DataBindingUtil; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.os.IBinder; | ||||
| import android.support.design.widget.Snackbar; | ||||
|  | @ -13,8 +15,11 @@ import android.support.v7.app.AlertDialog; | |||
| import android.support.v7.widget.Toolbar; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import eu.siacs.conversations.Config; | ||||
|  | @ -23,6 +28,7 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding; | |||
| import eu.siacs.conversations.databinding.DialogEnterPasswordBinding; | ||||
| import eu.siacs.conversations.services.ImportBackupService; | ||||
| import eu.siacs.conversations.ui.adapter.BackupFileAdapter; | ||||
| import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; | ||||
| import eu.siacs.conversations.utils.ThemeHelper; | ||||
| 
 | ||||
| public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { | ||||
|  | @ -32,6 +38,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
|     private BackupFileAdapter backupFileAdapter; | ||||
|     private ImportBackupService service; | ||||
| 
 | ||||
|     private boolean mLoadingState = false; | ||||
| 
 | ||||
|     private int mTheme; | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -47,6 +55,14 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
|         this.backupFileAdapter.setOnItemClickedListener(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(final Menu menu) { | ||||
|         getMenuInflater().inflate(R.menu.import_backup, menu); | ||||
|         final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file); | ||||
|         openBackup.setVisible(!this.mLoadingState); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onStart() { | ||||
|         super.onStart(); | ||||
|  | @ -87,9 +103,22 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onClick(ImportBackupService.BackupFile backupFile) { | ||||
|     public void onClick(final ImportBackupService.BackupFile backupFile) { | ||||
|         showEnterPasswordDialog(backupFile); | ||||
|     } | ||||
| 
 | ||||
|     private void openBackupFileFromUri(final Uri uri) { | ||||
|         try { | ||||
|             final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); | ||||
|             showEnterPasswordDialog(backupFile); | ||||
|         } catch (IOException e) { | ||||
|             Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile) { | ||||
|         final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); | ||||
|         Log.d(Config.LOGTAG, "attempting to import " + backupFile.getFile().getAbsolutePath()); | ||||
|         Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri()); | ||||
|         enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString())); | ||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(this); | ||||
|         builder.setView(enterPasswordBinding.getRoot()); | ||||
|  | @ -97,9 +126,16 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
|         builder.setNegativeButton(R.string.cancel, null); | ||||
|         builder.setPositiveButton(R.string.restore, (dialog, which) -> { | ||||
|             final String password = enterPasswordBinding.accountPassword.getEditableText().toString(); | ||||
|             final Uri uri = backupFile.getUri(); | ||||
|             Intent intent = new Intent(this, ImportBackupService.class); | ||||
|             intent.setAction(Intent.ACTION_SEND); | ||||
|             intent.putExtra("password", password); | ||||
|             intent.putExtra("file", backupFile.getFile().getAbsolutePath()); | ||||
|             if ("file".equals(uri.getScheme())) { | ||||
|                 intent.putExtra("file", uri.getPath()); | ||||
|             } else { | ||||
|                 intent.setData(uri); | ||||
|                 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
|             } | ||||
|             setLoadingState(true); | ||||
|             ContextCompat.startForegroundService(this, intent); | ||||
|         }); | ||||
|  | @ -108,10 +144,29 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
|     } | ||||
| 
 | ||||
|     private void setLoadingState(final boolean loadingState) { | ||||
|         binding.coordinator.setVisibility(loadingState ? View.GONE :View.VISIBLE); | ||||
|         binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); | ||||
|         binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); | ||||
|         setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); | ||||
|         configureActionBar(getSupportActionBar(),!loadingState); | ||||
|         configureActionBar(getSupportActionBar(), !loadingState); | ||||
|         this.mLoadingState = loadingState; | ||||
|         invalidateOptionsMenu(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onActivityResult(int requestCode, int resultCode, Intent intent) { | ||||
|         if (resultCode == RESULT_OK) { | ||||
|             if (requestCode == 0xbac) { | ||||
|                 openBackupFileFromUri(intent.getData()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAccountAlreadySetup() { | ||||
|         runOnUiThread(() -> { | ||||
|             setLoadingState(false); | ||||
|             Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -126,17 +181,33 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo | |||
| 
 | ||||
|     @Override | ||||
|     public void onBackupDecryptionFailed() { | ||||
|         runOnUiThread(()-> { | ||||
|         runOnUiThread(() -> { | ||||
|             setLoadingState(false); | ||||
|             Snackbar.make(binding.coordinator,R.string.unable_to_decrypt_backup,Snackbar.LENGTH_LONG).show(); | ||||
|             Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBackupRestoreFailed() { | ||||
|         runOnUiThread(()-> { | ||||
|         runOnUiThread(() -> { | ||||
|             setLoadingState(false); | ||||
|             Snackbar.make(binding.coordinator,R.string.unable_to_restore_backup,Snackbar.LENGTH_LONG).show(); | ||||
|             Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.action_open_backup_file: | ||||
|                 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); | ||||
|                 intent.setType("*/*"); | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | ||||
|                     intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); | ||||
|                 } | ||||
|                 intent.addCategory(Intent.CATEGORY_OPENABLE); | ||||
|                 startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac); | ||||
|                 return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
| } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 353 B | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 242 B | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 417 B | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 610 B | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 789 B | 
|  | @ -0,0 +1,11 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/action_open_backup_file" | ||||
|         android:icon="?attr/ic_cloud_download" | ||||
|         app:showAsAction="always" | ||||
|         android:title="@string/open_backup"/> | ||||
| 
 | ||||
| </menu> | ||||
|  | @ -43,6 +43,9 @@ | |||
|     <attr name="ic_attach_photo" format="reference"/> | ||||
|     <attr name="ic_attach_record" format="reference"/> | ||||
| 
 | ||||
| 
 | ||||
|     <attr name="ic_cloud_download" format="reference"/> | ||||
| 
 | ||||
|     <attr name="message_bubble_received_monochrome" format="reference"/> | ||||
|     <attr name="message_bubble_sent" format="reference"/> | ||||
|     <attr name="message_bubble_received_green" format="reference"/> | ||||
|  |  | |||
|  | @ -871,4 +871,7 @@ | |||
|     <string name="share_backup_files">Share backup files</string> | ||||
|     <string name="conversations_backup">Conversations backup</string> | ||||
|     <string name="event">Event</string> | ||||
|     <string name="open_backup">Open backup</string> | ||||
|     <string name="not_a_backup_file">The file you selected is not a Conversations backup file</string> | ||||
|     <string name="account_already_setup">This account has already been setup</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -98,6 +98,7 @@ | |||
|         <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item> | ||||
|         <item type="reference" name="icon_settings">@drawable/ic_settings_black_24dp</item> | ||||
|         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item> | ||||
|         <item type="reference" name="ic_cloud_download">@drawable/ic_cloud_download_white_24dp</item> | ||||
|         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item> | ||||
|         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_black</item> | ||||
| 
 | ||||
|  | @ -212,6 +213,7 @@ | |||
|         <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item> | ||||
|         <item type="reference" name="icon_settings">@drawable/ic_settings_white_24dp</item> | ||||
|         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item> | ||||
|         <item type="reference" name="ic_cloud_download">@drawable/ic_cloud_download_white_24dp</item> | ||||
|         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item> | ||||
|         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_white</item> | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Daniel Gultsch
						Daniel Gultsch