refactor ExportBackupService to worker
This commit is contained in:
parent
cbd8fb3488
commit
45b9c4dcc9
|
@ -50,6 +50,7 @@ dependencies {
|
||||||
implementation "androidx.preference:preference:1.2.1"
|
implementation "androidx.preference:preference:1.2.1"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
implementation 'androidx.work:work-runtime:2.9.0'
|
||||||
|
|
||||||
implementation "androidx.emoji2:emoji2:1.4.0"
|
implementation "androidx.emoji2:emoji2:1.4.0"
|
||||||
freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
|
freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
|
||||||
|
|
|
@ -37,6 +37,7 @@ import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.ui.ManageAccountActivity;
|
import eu.siacs.conversations.ui.ManageAccountActivity;
|
||||||
import eu.siacs.conversations.utils.BackupFileHeader;
|
import eu.siacs.conversations.utils.BackupFileHeader;
|
||||||
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
|
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
|
||||||
|
import eu.siacs.conversations.worker.ExportBackupWorker;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.engines.AESEngine;
|
import org.bouncycastle.crypto.engines.AESEngine;
|
||||||
|
@ -273,7 +274,7 @@ public class ImportBackupService extends Service {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
|
final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt());
|
||||||
|
|
||||||
final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||||
cipher.init(
|
cipher.init(
|
||||||
|
|
|
@ -116,9 +116,9 @@
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.ExportBackupService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:exported="false"
|
android:foregroundServiceType="dataSync"
|
||||||
android:foregroundServiceType="dataSync" />
|
tools:node="merge" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.ImportBackupService"
|
android:name=".services.ImportBackupService"
|
||||||
|
|
|
@ -23,10 +23,10 @@ import com.google.common.base.Strings;
|
||||||
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.ItemMediaBinding;
|
import eu.siacs.conversations.databinding.ItemMediaBinding;
|
||||||
import eu.siacs.conversations.services.ExportBackupService;
|
|
||||||
import eu.siacs.conversations.ui.XmppActivity;
|
import eu.siacs.conversations.ui.XmppActivity;
|
||||||
import eu.siacs.conversations.ui.util.Attachment;
|
import eu.siacs.conversations.ui.util.Attachment;
|
||||||
import eu.siacs.conversations.ui.util.ViewUtil;
|
import eu.siacs.conversations.ui.util.ViewUtil;
|
||||||
|
import eu.siacs.conversations.worker.ExportBackupWorker;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -99,7 +99,7 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
|
||||||
} else if (mime.equals("application/epub+zip")
|
} else if (mime.equals("application/epub+zip")
|
||||||
|| mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
|| mime.equals("application/vnd.amazon.mobi8-ebook")) {
|
||||||
return R.drawable.ic_book_48dp;
|
return R.drawable.ic_book_48dp;
|
||||||
} else if (mime.equals(ExportBackupService.MIME_TYPE)) {
|
} else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
|
||||||
return R.drawable.ic_backup_48dp;
|
return R.drawable.ic_backup_48dp;
|
||||||
} else if (DOCUMENT_MIMES.contains(mime)) {
|
} else if (DOCUMENT_MIMES.contains(mime)) {
|
||||||
return R.drawable.ic_description_48dp;
|
return R.drawable.ic_description_48dp;
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
package eu.siacs.conversations.ui.fragment.settings;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.work.Constraints;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.OutOfQuotaPolicy;
|
||||||
|
import androidx.work.PeriodicWorkRequest;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.R;
|
||||||
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
|
import eu.siacs.conversations.worker.ExportBackupWorker;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class BackupSettingsFragment extends XmppPreferenceFragment {
|
||||||
|
|
||||||
|
private static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup";
|
||||||
|
private static final String RECURRING_BACKUP = "recurring_backup";
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.RequestPermission(),
|
||||||
|
isGranted -> {
|
||||||
|
if (isGranted) {
|
||||||
|
startOneOffBackup();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireActivity(),
|
||||||
|
getString(
|
||||||
|
R.string.no_storage_permission,
|
||||||
|
getString(R.string.app_name)),
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.preferences_backup, rootKey);
|
||||||
|
final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
|
||||||
|
final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
|
||||||
|
final var backupDirectory = findPreference("backup_directory");
|
||||||
|
if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"The preference resource file is missing some preferences");
|
||||||
|
}
|
||||||
|
backupDirectory.setSummary(
|
||||||
|
getString(
|
||||||
|
R.string.pref_create_backup_summary,
|
||||||
|
FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
|
||||||
|
createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
|
||||||
|
final int[] choices = getResources().getIntArray(R.array.recurring_backup_values);
|
||||||
|
final CharSequence[] entries = new CharSequence[choices.length];
|
||||||
|
final CharSequence[] entryValues = new CharSequence[choices.length];
|
||||||
|
for (int i = 0; i < choices.length; ++i) {
|
||||||
|
entryValues[i] = String.valueOf(choices[i]);
|
||||||
|
entries[i] = timeframeValueToName(requireContext(), choices[i]);
|
||||||
|
}
|
||||||
|
recurringBackup.setEntries(entries);
|
||||||
|
recurringBackup.setEntryValues(entryValues);
|
||||||
|
recurringBackup.setSummaryProvider(new TimeframeSummaryProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSharedPreferenceChanged(@NonNull String key) {
|
||||||
|
super.onSharedPreferenceChanged(key);
|
||||||
|
if (RECURRING_BACKUP.equals(key)) {
|
||||||
|
final var sharedPreferences = getPreferenceManager().getSharedPreferences();
|
||||||
|
if (sharedPreferences == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Long recurringBackupInterval =
|
||||||
|
Longs.tryParse(
|
||||||
|
Strings.nullToEmpty(
|
||||||
|
sharedPreferences.getString(RECURRING_BACKUP, null)));
|
||||||
|
if (recurringBackupInterval == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"recurring backup interval changed to: " + recurringBackupInterval);
|
||||||
|
final var workManager = WorkManager.getInstance(requireContext());
|
||||||
|
if (recurringBackupInterval <= 0) {
|
||||||
|
workManager.cancelUniqueWork(RECURRING_BACKUP);
|
||||||
|
} else {
|
||||||
|
final Constraints constraints =
|
||||||
|
new Constraints.Builder()
|
||||||
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.setRequiresStorageNotLow(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final PeriodicWorkRequest periodicWorkRequest =
|
||||||
|
new PeriodicWorkRequest.Builder(
|
||||||
|
ExportBackupWorker.class,
|
||||||
|
recurringBackupInterval,
|
||||||
|
TimeUnit.SECONDS)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setInputData(
|
||||||
|
new Data.Builder()
|
||||||
|
.putBoolean("recurring_backup", true)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
RECURRING_BACKUP, ExistingPeriodicWorkPolicy.UPDATE, periodicWorkRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
requireActivity().setTitle(R.string.backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onBackupPreferenceClicked(final Preference preference) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||||
|
} else {
|
||||||
|
startOneOffBackup();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startOneOffBackup();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startOneOffBackup() {
|
||||||
|
final OneTimeWorkRequest exportBackupWorkRequest =
|
||||||
|
new OneTimeWorkRequest.Builder(ExportBackupWorker.class)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build();
|
||||||
|
WorkManager.getInstance(requireContext())
|
||||||
|
.enqueueUniqueWork(
|
||||||
|
CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
|
||||||
|
final MaterialAlertDialogBuilder builder =
|
||||||
|
new MaterialAlertDialogBuilder(requireActivity());
|
||||||
|
builder.setMessage(R.string.backup_started_message);
|
||||||
|
builder.setPositiveButton(R.string.ok, null);
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,63 +1,27 @@
|
||||||
package eu.siacs.conversations.ui.fragment.settings;
|
package eu.siacs.conversations.ui.fragment.settings;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import eu.siacs.conversations.BuildConfig;
|
import eu.siacs.conversations.BuildConfig;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
|
||||||
import eu.siacs.conversations.services.ExportBackupService;
|
|
||||||
|
|
||||||
public class MainSettingsFragment extends PreferenceFragmentCompat {
|
public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
private static final String CREATE_BACKUP = "create_backup";
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
|
|
||||||
registerForActivityResult(
|
|
||||||
new ActivityResultContracts.RequestPermission(),
|
|
||||||
isGranted -> {
|
|
||||||
if (isGranted) {
|
|
||||||
startBackup();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
requireActivity(),
|
|
||||||
getString(
|
|
||||||
R.string.no_storage_permission,
|
|
||||||
getString(R.string.app_name)),
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||||
setPreferencesFromResource(R.xml.preferences_main, rootKey);
|
setPreferencesFromResource(R.xml.preferences_main, rootKey);
|
||||||
final var about = findPreference("about");
|
final var about = findPreference("about");
|
||||||
final var connection = findPreference("connection");
|
final var connection = findPreference("connection");
|
||||||
final var backup = findPreference(CREATE_BACKUP);
|
if (about == null || connection == null) {
|
||||||
if (about == null || connection == null || backup == null) {
|
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"The preference resource file is missing some preferences");
|
"The preference resource file is missing some preferences");
|
||||||
}
|
}
|
||||||
backup.setSummary(
|
|
||||||
getString(
|
|
||||||
R.string.pref_create_backup_summary,
|
|
||||||
FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
|
|
||||||
backup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
|
|
||||||
about.setTitle(getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
|
about.setTitle(getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
|
||||||
about.setSummary(
|
about.setSummary(
|
||||||
String.format(
|
String.format(
|
||||||
|
@ -73,31 +37,6 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onBackupPreferenceClicked(final Preference preference) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
if (ContextCompat.checkSelfPermission(
|
|
||||||
requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
|
||||||
requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
|
||||||
} else {
|
|
||||||
startBackup();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startBackup();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startBackup() {
|
|
||||||
ContextCompat.startForegroundService(
|
|
||||||
requireContext(), new Intent(requireContext(), ExportBackupService.class));
|
|
||||||
final MaterialAlertDialogBuilder builder =
|
|
||||||
new MaterialAlertDialogBuilder(requireActivity());
|
|
||||||
builder.setMessage(R.string.backup_started_message);
|
|
||||||
builder.setPositiveButton(R.string.ok, null);
|
|
||||||
builder.create().show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.siacs.conversations.ui.fragment.settings;
|
package eu.siacs.conversations.ui.fragment.settings;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
@ -13,13 +12,11 @@ import androidx.preference.Preference;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.primitives.Ints;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.AppSettings;
|
import eu.siacs.conversations.AppSettings;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.OmemoSetting;
|
import eu.siacs.conversations.crypto.OmemoSetting;
|
||||||
import eu.siacs.conversations.services.MemorizingTrustManager;
|
import eu.siacs.conversations.services.MemorizingTrustManager;
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
|
||||||
|
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -44,20 +41,13 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
|
||||||
final CharSequence[] entryValues = new CharSequence[choices.length];
|
final CharSequence[] entryValues = new CharSequence[choices.length];
|
||||||
for (int i = 0; i < choices.length; ++i) {
|
for (int i = 0; i < choices.length; ++i) {
|
||||||
entryValues[i] = String.valueOf(choices[i]);
|
entryValues[i] = String.valueOf(choices[i]);
|
||||||
entries[i] = messageDeletionValueToName(requireContext(), choices[i]);
|
entries[i] = timeframeValueToName(requireContext(), choices[i]);
|
||||||
}
|
}
|
||||||
automaticMessageDeletion.setEntries(entries);
|
automaticMessageDeletion.setEntries(entries);
|
||||||
automaticMessageDeletion.setEntryValues(entryValues);
|
automaticMessageDeletion.setEntryValues(entryValues);
|
||||||
automaticMessageDeletion.setSummaryProvider(new MessageDeletionSummaryProvider());
|
automaticMessageDeletion.setSummaryProvider(new TimeframeSummaryProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String messageDeletionValueToName(final Context context, final int value) {
|
|
||||||
if (value == 0) {
|
|
||||||
return context.getString(R.string.never);
|
|
||||||
} else {
|
|
||||||
return TimeFrameUtils.resolve(context, 1000L * value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSharedPreferenceChanged(@NonNull String key) {
|
protected void onSharedPreferenceChanged(@NonNull String key) {
|
||||||
|
@ -161,16 +151,6 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class MessageDeletionSummaryProvider
|
|
||||||
implements Preference.SummaryProvider<ListPreference> {
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
|
||||||
final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
|
|
||||||
return messageDeletionValueToName(preference.getContext(), value == null ? 0 : value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class OmemoSummaryProvider
|
private static class OmemoSummaryProvider
|
||||||
implements Preference.SummaryProvider<ListPreference> {
|
implements Preference.SummaryProvider<ListPreference> {
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
package eu.siacs.conversations.ui.fragment.settings;
|
package eu.siacs.conversations.ui.fragment.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.XmppActivity;
|
import eu.siacs.conversations.ui.XmppActivity;
|
||||||
|
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||||
|
|
||||||
public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
|
public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@ -83,4 +92,23 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
|
||||||
protected void runOnUiThread(final Runnable runnable) {
|
protected void runOnUiThread(final Runnable runnable) {
|
||||||
requireActivity().runOnUiThread(runnable);
|
requireActivity().runOnUiThread(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static String timeframeValueToName(final Context context, final int value) {
|
||||||
|
if (value == 0) {
|
||||||
|
return context.getString(R.string.never);
|
||||||
|
} else {
|
||||||
|
return TimeFrameUtils.resolve(context, 1000L * value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class TimeframeSummaryProvider
|
||||||
|
implements Preference.SummaryProvider<ListPreference> {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
||||||
|
final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
|
||||||
|
return timeframeValueToName(preference.getContext(), value == null ? 0 : value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import java.util.Properties;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.services.ExportBackupService;
|
import eu.siacs.conversations.worker.ExportBackupWorker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for dealing with MIME types.
|
* Utilities for dealing with MIME types.
|
||||||
|
@ -91,7 +91,7 @@ public final class MimeUtils {
|
||||||
add("application/vnd.amazon.mobi8-ebook", "kfx");
|
add("application/vnd.amazon.mobi8-ebook", "kfx");
|
||||||
add("application/vnd.android.package-archive", "apk");
|
add("application/vnd.android.package-archive", "apk");
|
||||||
add("application/vnd.cinderella", "cdy");
|
add("application/vnd.cinderella", "cdy");
|
||||||
add(ExportBackupService.MIME_TYPE, "ceb");
|
add(ExportBackupWorker.MIME_TYPE, "ceb");
|
||||||
add("application/vnd.ms-pki.stl", "stl");
|
add("application/vnd.ms-pki.stl", "stl");
|
||||||
add("application/vnd.oasis.opendocument.database", "odb");
|
add("application/vnd.oasis.opendocument.database", "odb");
|
||||||
add("application/vnd.oasis.opendocument.formula", "odf");
|
add("application/vnd.oasis.opendocument.formula", "odf");
|
||||||
|
|
|
@ -16,8 +16,6 @@ import androidx.core.content.ContextCompat;
|
||||||
import com.google.android.material.color.MaterialColors;
|
import com.google.android.material.color.MaterialColors;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -31,14 +29,13 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.ListItem;
|
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.MucOptions;
|
import eu.siacs.conversations.entities.MucOptions;
|
||||||
import eu.siacs.conversations.entities.Presence;
|
import eu.siacs.conversations.entities.Presence;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.services.ExportBackupService;
|
|
||||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
import eu.siacs.conversations.ui.util.QuoteHelper;
|
||||||
|
import eu.siacs.conversations.worker.ExportBackupWorker;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public class UIHelper {
|
public class UIHelper {
|
||||||
|
@ -410,7 +407,7 @@ public class UIHelper {
|
||||||
return context.getString(R.string.pdf_document);
|
return context.getString(R.string.pdf_document);
|
||||||
} else if (mime.equals("application/vnd.android.package-archive")) {
|
} else if (mime.equals("application/vnd.android.package-archive")) {
|
||||||
return context.getString(R.string.apk);
|
return context.getString(R.string.apk);
|
||||||
} else if (mime.equals(ExportBackupService.MIME_TYPE)) {
|
} else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
|
||||||
return context.getString(R.string.conversations_backup);
|
return context.getString(R.string.conversations_backup);
|
||||||
} else if (mime.contains("vcard")) {
|
} else if (mime.contains("vcard")) {
|
||||||
return context.getString(R.string.vcard);
|
return context.getString(R.string.vcard);
|
||||||
|
|
|
@ -1,53 +1,28 @@
|
||||||
package eu.siacs.conversations.services;
|
package eu.siacs.conversations.worker;
|
||||||
|
|
||||||
import static eu.siacs.conversations.utils.Compatibility.s;
|
import static eu.siacs.conversations.utils.Compatibility.s;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.IBinder;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.work.ForegroundInfo;
|
||||||
|
import androidx.work.Worker;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.zip.GZIPOutputStream;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.CipherOutputStream;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
|
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
|
||||||
|
@ -59,9 +34,38 @@ import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.utils.BackupFileHeader;
|
import eu.siacs.conversations.utils.BackupFileHeader;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
|
||||||
public class ExportBackupService extends Service {
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class ExportBackupWorker extends Worker {
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FORMAT =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||||
|
|
||||||
public static final String KEYTYPE = "AES";
|
public static final String KEYTYPE = "AES";
|
||||||
public static final String CIPHERMODE = "AES/GCM/NoPadding";
|
public static final String CIPHERMODE = "AES/GCM/NoPadding";
|
||||||
|
@ -70,10 +74,362 @@ public class ExportBackupService extends Service {
|
||||||
public static final String MIME_TYPE = "application/vnd.conversations.backup";
|
public static final String MIME_TYPE = "application/vnd.conversations.backup";
|
||||||
|
|
||||||
private static final int NOTIFICATION_ID = 19;
|
private static final int NOTIFICATION_ID = 19;
|
||||||
private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
|
private static final int BACKUP_CREATED_NOTIFICATION_ID = 23;
|
||||||
private DatabaseBackend mDatabaseBackend;
|
|
||||||
private List<Account> mAccounts;
|
private final boolean recurringBackup;
|
||||||
private NotificationManager notificationManager;
|
|
||||||
|
public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||||
|
super(context, workerParams);
|
||||||
|
final var inputData = workerParams.getInputData();
|
||||||
|
this.recurringBackup = inputData.getBoolean("recurring_backup", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Result doWork() {
|
||||||
|
final List<File> files;
|
||||||
|
try {
|
||||||
|
files = export();
|
||||||
|
} catch (final IOException
|
||||||
|
| InvalidKeySpecException
|
||||||
|
| InvalidAlgorithmParameterException
|
||||||
|
| InvalidKeyException
|
||||||
|
| NoSuchPaddingException
|
||||||
|
| NoSuchAlgorithmException
|
||||||
|
| NoSuchProviderException e) {
|
||||||
|
Log.d(Config.LOGTAG, "could not create backup", e);
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, "done creating " + files.size() + " backup files");
|
||||||
|
getApplicationContext().getSystemService(NotificationManager.class).cancel(NOTIFICATION_ID);
|
||||||
|
if (files.isEmpty() || recurringBackup) {
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
notifySuccess(files);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ForegroundInfo getForegroundInfo() {
|
||||||
|
Log.d(Config.LOGTAG, "getForegroundInfo()");
|
||||||
|
final var context = getApplicationContext();
|
||||||
|
final NotificationCompat.Builder notification =
|
||||||
|
new NotificationCompat.Builder(context, "backup");
|
||||||
|
notification
|
||||||
|
.setContentTitle(context.getString(R.string.notification_create_backup_title))
|
||||||
|
.setSmallIcon(R.drawable.ic_archive_24dp)
|
||||||
|
.setProgress(1, 0, false);
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
return new ForegroundInfo(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
notification.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||||
|
} else {
|
||||||
|
return new ForegroundInfo(NOTIFICATION_ID, notification.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<File> export()
|
||||||
|
throws IOException,
|
||||||
|
InvalidKeySpecException,
|
||||||
|
InvalidAlgorithmParameterException,
|
||||||
|
InvalidKeyException,
|
||||||
|
NoSuchPaddingException,
|
||||||
|
NoSuchAlgorithmException,
|
||||||
|
NoSuchProviderException {
|
||||||
|
final Context context = getApplicationContext();
|
||||||
|
final var database = DatabaseBackend.getInstance(context);
|
||||||
|
final var accounts = database.getAccounts();
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
final int max = accounts.size();
|
||||||
|
final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
final List<File> files = new ArrayList<>();
|
||||||
|
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
|
||||||
|
for (final Account account : accounts) {
|
||||||
|
final String password = account.getPassword();
|
||||||
|
if (Strings.nullToEmpty(password).trim().isEmpty()) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
String.format(
|
||||||
|
"skipping backup for %s because password is empty. unable to encrypt",
|
||||||
|
account.getJid().asBareJid()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
String.format(
|
||||||
|
"exporting data for account %s (%s)",
|
||||||
|
account.getJid().asBareJid(), account.getUuid()));
|
||||||
|
final byte[] IV = new byte[12];
|
||||||
|
final byte[] salt = new byte[16];
|
||||||
|
secureRandom.nextBytes(IV);
|
||||||
|
secureRandom.nextBytes(salt);
|
||||||
|
final BackupFileHeader backupFileHeader =
|
||||||
|
new BackupFileHeader(
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
account.getJid(),
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
IV,
|
||||||
|
salt);
|
||||||
|
final NotificationCompat.Builder notification =
|
||||||
|
new NotificationCompat.Builder(context, "backup");
|
||||||
|
notification
|
||||||
|
.setContentTitle(context.getString(R.string.notification_create_backup_title))
|
||||||
|
.setSmallIcon(R.drawable.ic_archive_24dp)
|
||||||
|
.setProgress(1, 0, false);
|
||||||
|
final Progress progress = new Progress(notification, max, count);
|
||||||
|
final String filename =
|
||||||
|
String.format(
|
||||||
|
"%s.%s.ceb",
|
||||||
|
account.getJid().asBareJid().toEscapedString(),
|
||||||
|
DATE_FORMAT.format(new Date()));
|
||||||
|
final File file = new File(FileBackend.getBackupDirectory(context), filename);
|
||||||
|
files.add(file);
|
||||||
|
final File directory = file.getParentFile();
|
||||||
|
if (directory != null && directory.mkdirs()) {
|
||||||
|
Log.d(Config.LOGTAG, "created backup directory " + directory.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);
|
||||||
|
final byte[] key = getKey(password, salt);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
CipherOutputStream cipherOutputStream =
|
||||||
|
new CipherOutputStream(fileOutputStream, cipher);
|
||||||
|
|
||||||
|
final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
|
||||||
|
final JsonWriter jsonWriter =
|
||||||
|
new JsonWriter(
|
||||||
|
new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8));
|
||||||
|
jsonWriter.beginArray();
|
||||||
|
final SQLiteDatabase db = database.getReadableDatabase();
|
||||||
|
final String uuid = account.getUuid();
|
||||||
|
accountExport(db, uuid, jsonWriter);
|
||||||
|
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter);
|
||||||
|
messageExport(db, uuid, jsonWriter, progress);
|
||||||
|
for (final String table :
|
||||||
|
Arrays.asList(
|
||||||
|
SQLiteAxolotlStore.PREKEY_TABLENAME,
|
||||||
|
SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
|
||||||
|
SQLiteAxolotlStore.SESSION_TABLENAME,
|
||||||
|
SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
|
||||||
|
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter);
|
||||||
|
}
|
||||||
|
jsonWriter.endArray();
|
||||||
|
jsonWriter.flush();
|
||||||
|
jsonWriter.close();
|
||||||
|
mediaScannerScanFile(file);
|
||||||
|
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mediaScannerScanFile(final File file) {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||||
|
intent.setData(Uri.fromFile(file));
|
||||||
|
getApplicationContext().sendBroadcast(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void accountExport(
|
||||||
|
final SQLiteDatabase db, final String uuid, final JsonWriter writer)
|
||||||
|
throws IOException {
|
||||||
|
try (final Cursor accountCursor =
|
||||||
|
db.query(
|
||||||
|
Account.TABLENAME,
|
||||||
|
null,
|
||||||
|
Account.UUID + "=?",
|
||||||
|
new String[] {uuid},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)) {
|
||||||
|
while (accountCursor != null && accountCursor.moveToNext()) {
|
||||||
|
writer.beginObject();
|
||||||
|
writer.name("table");
|
||||||
|
writer.value(Account.TABLENAME);
|
||||||
|
writer.name("values");
|
||||||
|
writer.beginObject();
|
||||||
|
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
|
||||||
|
final String name = accountCursor.getColumnName(i);
|
||||||
|
writer.name(name);
|
||||||
|
final String value = accountCursor.getString(i);
|
||||||
|
if (value == null
|
||||||
|
|| Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
|
||||||
|
writer.nullValue();
|
||||||
|
} else if (Account.OPTIONS.equals(accountCursor.getColumnName(i))
|
||||||
|
&& value.matches("\\d+")) {
|
||||||
|
int intValue = Integer.parseInt(value);
|
||||||
|
intValue |= 1 << Account.OPTION_DISABLED;
|
||||||
|
writer.value(intValue);
|
||||||
|
} else {
|
||||||
|
writer.value(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.endObject();
|
||||||
|
writer.endObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void simpleExport(
|
||||||
|
final SQLiteDatabase db,
|
||||||
|
final String table,
|
||||||
|
final String column,
|
||||||
|
final String uuid,
|
||||||
|
final JsonWriter writer)
|
||||||
|
throws IOException {
|
||||||
|
try (final Cursor cursor =
|
||||||
|
db.query(table, null, column + "=?", new String[] {uuid}, null, null, null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
writer.beginObject();
|
||||||
|
writer.name("table");
|
||||||
|
writer.value(table);
|
||||||
|
writer.name("values");
|
||||||
|
writer.beginObject();
|
||||||
|
for (int i = 0; i < cursor.getColumnCount(); ++i) {
|
||||||
|
final String name = cursor.getColumnName(i);
|
||||||
|
writer.name(name);
|
||||||
|
final String value = cursor.getString(i);
|
||||||
|
writer.value(value);
|
||||||
|
}
|
||||||
|
writer.endObject();
|
||||||
|
writer.endObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void messageExport(
|
||||||
|
final SQLiteDatabase db,
|
||||||
|
final String uuid,
|
||||||
|
final JsonWriter writer,
|
||||||
|
final Progress progress)
|
||||||
|
throws IOException {
|
||||||
|
final var notificationManager =
|
||||||
|
getApplicationContext().getSystemService(NotificationManager.class);
|
||||||
|
try (final Cursor cursor =
|
||||||
|
db.rawQuery(
|
||||||
|
"select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?",
|
||||||
|
new String[] {uuid})) {
|
||||||
|
final int size = cursor != null ? cursor.getCount() : 0;
|
||||||
|
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
|
||||||
|
int i = 0;
|
||||||
|
int p = 0;
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
writer.beginObject();
|
||||||
|
writer.name("table");
|
||||||
|
writer.value(Message.TABLENAME);
|
||||||
|
writer.name("values");
|
||||||
|
writer.beginObject();
|
||||||
|
for (int j = 0; j < cursor.getColumnCount(); ++j) {
|
||||||
|
final String name = cursor.getColumnName(j);
|
||||||
|
writer.name(name);
|
||||||
|
final String value = cursor.getString(j);
|
||||||
|
writer.value(value);
|
||||||
|
}
|
||||||
|
writer.endObject();
|
||||||
|
writer.endObject();
|
||||||
|
final int percentage = i * 100 / size;
|
||||||
|
if (p < percentage) {
|
||||||
|
p = percentage;
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getKey(final String password, final byte[] salt)
|
||||||
|
throws InvalidKeySpecException {
|
||||||
|
final SecretKeyFactory factory;
|
||||||
|
try {
|
||||||
|
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128))
|
||||||
|
.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySuccess(final List<File> files) {
|
||||||
|
final var context = getApplicationContext();
|
||||||
|
final String path = FileBackend.getBackupDirectory(context).getAbsolutePath();
|
||||||
|
|
||||||
|
final var openFolderIntent = getOpenFolderIntent(path);
|
||||||
|
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
|
||||||
|
final ArrayList<Uri> uris = new ArrayList<>();
|
||||||
|
for (final File file : files) {
|
||||||
|
uris.add(FileBackend.getUriForFile(context, 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, context.getString(R.string.share_backup_files));
|
||||||
|
final var shareFilesIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
190,
|
||||||
|
chooser,
|
||||||
|
s()
|
||||||
|
? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
: PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup");
|
||||||
|
mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title))
|
||||||
|
.setContentText(
|
||||||
|
context.getString(R.string.notification_backup_created_subtitle, path))
|
||||||
|
.setStyle(
|
||||||
|
new NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(
|
||||||
|
context.getString(
|
||||||
|
R.string.notification_backup_created_subtitle,
|
||||||
|
FileBackend.getBackupDirectory(context)
|
||||||
|
.getAbsolutePath())))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_archive_24dp);
|
||||||
|
|
||||||
|
if (openFolderIntent.isPresent()) {
|
||||||
|
mBuilder.setContentIntent(openFolderIntent.get());
|
||||||
|
} else {
|
||||||
|
Log.w(Config.LOGTAG, "no app can display folders");
|
||||||
|
}
|
||||||
|
|
||||||
|
mBuilder.addAction(
|
||||||
|
R.drawable.ic_share_24dp,
|
||||||
|
context.getString(R.string.share_backup_files),
|
||||||
|
shareFilesIntent);
|
||||||
|
final var notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
|
notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, mBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<PendingIntent> getOpenFolderIntent(final String path) {
|
||||||
|
final var context = getApplicationContext();
|
||||||
|
for (final Intent intent : getPossibleFileOpenIntents(context, path)) {
|
||||||
|
if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) {
|
||||||
|
return Optional.of(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
189,
|
||||||
|
intent,
|
||||||
|
s()
|
||||||
|
? PendingIntent.FLAG_IMMUTABLE
|
||||||
|
| PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
: PendingIntent.FLAG_UPDATE_CURRENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Intent> getPossibleFileOpenIntents(
|
private static List<Intent> getPossibleFileOpenIntents(
|
||||||
final Context context, final String path) {
|
final Context context, final String path) {
|
||||||
|
@ -101,356 +457,21 @@ public class ExportBackupService extends Service {
|
||||||
return Arrays.asList(openIntent, amazeIntent, systemFallBack);
|
return Arrays.asList(openIntent, amazeIntent, systemFallBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void accountExport(
|
|
||||||
final SQLiteDatabase db, final String uuid, final JsonWriter writer)
|
|
||||||
throws IOException {
|
|
||||||
final Cursor accountCursor =
|
|
||||||
db.query(
|
|
||||||
Account.TABLENAME,
|
|
||||||
null,
|
|
||||||
Account.UUID + "=?",
|
|
||||||
new String[] {uuid},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
while (accountCursor != null && accountCursor.moveToNext()) {
|
|
||||||
writer.beginObject();
|
|
||||||
writer.name("table");
|
|
||||||
writer.value(Account.TABLENAME);
|
|
||||||
writer.name("values");
|
|
||||||
writer.beginObject();
|
|
||||||
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
|
|
||||||
final String name = accountCursor.getColumnName(i);
|
|
||||||
writer.name(name);
|
|
||||||
final String value = accountCursor.getString(i);
|
|
||||||
if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
|
|
||||||
writer.nullValue();
|
|
||||||
} else if (Account.OPTIONS.equals(accountCursor.getColumnName(i))
|
|
||||||
&& value.matches("\\d+")) {
|
|
||||||
int intValue = Integer.parseInt(value);
|
|
||||||
intValue |= 1 << Account.OPTION_DISABLED;
|
|
||||||
writer.value(intValue);
|
|
||||||
} else {
|
|
||||||
writer.value(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.endObject();
|
|
||||||
writer.endObject();
|
|
||||||
}
|
|
||||||
if (accountCursor != null) {
|
|
||||||
accountCursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void simpleExport(
|
|
||||||
final SQLiteDatabase db,
|
|
||||||
final String table,
|
|
||||||
final String column,
|
|
||||||
final String uuid,
|
|
||||||
final JsonWriter writer)
|
|
||||||
throws IOException {
|
|
||||||
final Cursor cursor =
|
|
||||||
db.query(table, null, column + "=?", new String[] {uuid}, null, null, null);
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
writer.beginObject();
|
|
||||||
writer.name("table");
|
|
||||||
writer.value(table);
|
|
||||||
writer.name("values");
|
|
||||||
writer.beginObject();
|
|
||||||
for (int i = 0; i < cursor.getColumnCount(); ++i) {
|
|
||||||
final String name = cursor.getColumnName(i);
|
|
||||||
writer.name(name);
|
|
||||||
final String value = cursor.getString(i);
|
|
||||||
writer.value(value);
|
|
||||||
}
|
|
||||||
writer.endObject();
|
|
||||||
writer.endObject();
|
|
||||||
}
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] getKey(final String password, final byte[] salt)
|
|
||||||
throws InvalidKeySpecException {
|
|
||||||
final SecretKeyFactory factory;
|
|
||||||
try {
|
|
||||||
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128))
|
|
||||||
.getEncoded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(
|
|
||||||
() -> {
|
|
||||||
boolean success;
|
|
||||||
List<File> files;
|
|
||||||
try {
|
|
||||||
files = export();
|
|
||||||
success = true;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
Log.d(Config.LOGTAG, "unable to create backup", e);
|
|
||||||
success = false;
|
|
||||||
files = Collections.emptyList();
|
|
||||||
}
|
|
||||||
stopForeground(true);
|
|
||||||
RUNNING.set(false);
|
|
||||||
if (success) {
|
|
||||||
notifySuccess(files);
|
|
||||||
}
|
|
||||||
stopSelf();
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
return START_STICKY;
|
|
||||||
} else {
|
|
||||||
Log.d(
|
|
||||||
Config.LOGTAG,
|
|
||||||
"ExportBackupService. ignoring start command because already running");
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void messageExport(
|
|
||||||
final SQLiteDatabase db,
|
|
||||||
final String uuid,
|
|
||||||
final JsonWriter writer,
|
|
||||||
final Progress progress)
|
|
||||||
throws IOException {
|
|
||||||
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;
|
|
||||||
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
|
|
||||||
int i = 0;
|
|
||||||
int p = 0;
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
writer.beginObject();
|
|
||||||
writer.name("table");
|
|
||||||
writer.value(Message.TABLENAME);
|
|
||||||
writer.name("values");
|
|
||||||
writer.beginObject();
|
|
||||||
for (int j = 0; j < cursor.getColumnCount(); ++j) {
|
|
||||||
final String name = cursor.getColumnName(j);
|
|
||||||
writer.name(name);
|
|
||||||
final String value = cursor.getString(j);
|
|
||||||
writer.value(value);
|
|
||||||
}
|
|
||||||
writer.endObject();
|
|
||||||
writer.endObject();
|
|
||||||
final int percentage = i * 100 / size;
|
|
||||||
if (p < percentage) {
|
|
||||||
p = percentage;
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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_24dp)
|
|
||||||
.setProgress(1, 0, false);
|
|
||||||
startForeground(NOTIFICATION_ID, mBuilder.build());
|
|
||||||
int count = 0;
|
|
||||||
final int max = this.mAccounts.size();
|
|
||||||
final SecureRandom secureRandom = new SecureRandom();
|
|
||||||
final List<File> files = new ArrayList<>();
|
|
||||||
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
|
|
||||||
for (final Account account : this.mAccounts) {
|
|
||||||
final String password = account.getPassword();
|
|
||||||
if (Strings.nullToEmpty(password).trim().isEmpty()) {
|
|
||||||
Log.d(
|
|
||||||
Config.LOGTAG,
|
|
||||||
String.format(
|
|
||||||
"skipping backup for %s because password is empty. unable to encrypt",
|
|
||||||
account.getJid().asBareJid()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Log.d(
|
|
||||||
Config.LOGTAG,
|
|
||||||
String.format(
|
|
||||||
"exporting data for account %s (%s)",
|
|
||||||
account.getJid().asBareJid(), account.getUuid()));
|
|
||||||
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 String filename =
|
|
||||||
String.format(
|
|
||||||
"%s.%s.ceb",
|
|
||||||
account.getJid().asBareJid().toEscapedString(),
|
|
||||||
DATE_FORMAT.format(new Date()));
|
|
||||||
final File file =
|
|
||||||
new File(
|
|
||||||
FileBackend.getBackupDirectory(this), filename);
|
|
||||||
files.add(file);
|
|
||||||
final File directory = file.getParentFile();
|
|
||||||
if (directory != null && directory.mkdirs()) {
|
|
||||||
Log.d(Config.LOGTAG, "created backup directory " + directory.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);
|
|
||||||
final byte[] key = getKey(password, salt);
|
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
|
||||||
CipherOutputStream cipherOutputStream =
|
|
||||||
new CipherOutputStream(fileOutputStream, cipher);
|
|
||||||
|
|
||||||
final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
|
|
||||||
final JsonWriter jsonWriter =
|
|
||||||
new JsonWriter(
|
|
||||||
new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8));
|
|
||||||
jsonWriter.beginArray();
|
|
||||||
final SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
|
|
||||||
final String uuid = account.getUuid();
|
|
||||||
accountExport(db, uuid, jsonWriter);
|
|
||||||
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter);
|
|
||||||
messageExport(db, uuid, jsonWriter, progress);
|
|
||||||
for (final String table :
|
|
||||||
Arrays.asList(
|
|
||||||
SQLiteAxolotlStore.PREKEY_TABLENAME,
|
|
||||||
SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
|
|
||||||
SQLiteAxolotlStore.SESSION_TABLENAME,
|
|
||||||
SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
|
|
||||||
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter);
|
|
||||||
}
|
|
||||||
jsonWriter.endArray();
|
|
||||||
jsonWriter.flush();
|
|
||||||
jsonWriter.close();
|
|
||||||
mediaScannerScanFile(file);
|
|
||||||
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mediaScannerScanFile(final File file) {
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
|
||||||
intent.setData(Uri.fromFile(file));
|
|
||||||
sendBroadcast(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifySuccess(final List<File> files) {
|
|
||||||
final String path = FileBackend.getBackupDirectory(this).getAbsolutePath();
|
|
||||||
|
|
||||||
PendingIntent openFolderIntent = null;
|
|
||||||
|
|
||||||
for (final Intent intent : getPossibleFileOpenIntents(this, path)) {
|
|
||||||
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
|
|
||||||
openFolderIntent =
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
189,
|
|
||||||
intent,
|
|
||||||
s()
|
|
||||||
? PendingIntent.FLAG_IMMUTABLE
|
|
||||||
| PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
: 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,
|
|
||||||
s()
|
|
||||||
? PendingIntent.FLAG_IMMUTABLE
|
|
||||||
| PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
: 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)
|
|
||||||
.getAbsolutePath())))
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(openFolderIntent)
|
|
||||||
.setSmallIcon(R.drawable.ic_archive_24dp);
|
|
||||||
|
|
||||||
if (shareFilesIntent != null) {
|
|
||||||
mBuilder.addAction(
|
|
||||||
R.drawable.ic_share_24dp,
|
|
||||||
getString(R.string.share_backup_files),
|
|
||||||
shareFilesIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Progress {
|
private static class Progress {
|
||||||
private final NotificationCompat.Builder builder;
|
private final NotificationCompat.Builder notification;
|
||||||
private final int max;
|
private final int max;
|
||||||
private final int count;
|
private final int count;
|
||||||
|
|
||||||
private Progress(NotificationCompat.Builder builder, int max, int count) {
|
private Progress(
|
||||||
this.builder = builder;
|
final NotificationCompat.Builder notification, final int max, final int count) {
|
||||||
|
this.notification = notification;
|
||||||
this.max = max;
|
this.max = max;
|
||||||
this.count = count;
|
this.count = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Notification build(int percentage) {
|
private Notification build(int percentage) {
|
||||||
builder.setProgress(max * 100, count * 100 + percentage, false);
|
notification.setProgress(max * 100, count * 100 + percentage, false);
|
||||||
return builder.build();
|
return notification.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
12
src/main/res/drawable/ic_calendar_month_24dp.xml
Normal file
12
src/main/res/drawable/ic_calendar_month_24dp.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z" />
|
||||||
|
|
||||||
|
</vector>
|
|
@ -84,6 +84,13 @@
|
||||||
<item>2592000</item>
|
<item>2592000</item>
|
||||||
<item>15811200</item>
|
<item>15811200</item>
|
||||||
</integer-array>
|
</integer-array>
|
||||||
|
<integer-array name="recurring_backup_values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>86400</item>
|
||||||
|
<item>172800</item>
|
||||||
|
<item>604800</item>
|
||||||
|
<item>2592000</item>
|
||||||
|
</integer-array>
|
||||||
<string-array name="omemo_setting_entry_values">
|
<string-array name="omemo_setting_entry_values">
|
||||||
<item>always</item>
|
<item>always</item>
|
||||||
<item>default_on</item>
|
<item>default_on</item>
|
||||||
|
|
|
@ -1059,5 +1059,8 @@
|
||||||
<string name="pref_large_font_summary">Increase font size in message bubbles</string>
|
<string name="pref_large_font_summary">Increase font size in message bubbles</string>
|
||||||
<string name="pref_accept_invites_from_strangers">Invites from strangers</string>
|
<string name="pref_accept_invites_from_strangers">Invites from strangers</string>
|
||||||
<string name="pref_accept_invites_from_strangers_summary">Accept invites to group chats from strangers</string>
|
<string name="pref_accept_invites_from_strangers_summary">Accept invites to group chats from strangers</string>
|
||||||
|
<string name="pref_backup_summary">Create one-off, Schedule recurring</string>
|
||||||
|
<string name="pref_create_backup_one_off_summary">Create one-off backup</string>
|
||||||
|
<string name="pref_backup_recurring">Recurring backup</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
20
src/main/res/xml/preferences_backup.xml
Normal file
20
src/main/res/xml/preferences_backup.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="@integer/automatic_message_deletion"
|
||||||
|
android:icon="@drawable/ic_calendar_month_24dp"
|
||||||
|
android:key="recurring_backup"
|
||||||
|
android:title="@string/pref_backup_recurring" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_archive_24dp"
|
||||||
|
android:key="create_one_off_backup"
|
||||||
|
android:summary="@string/pref_create_backup_one_off_summary"
|
||||||
|
android:title="@string/pref_create_backup" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="backup_directory"
|
||||||
|
android:summary="@string/pref_create_backup_summary" />
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
|
@ -34,9 +34,10 @@
|
||||||
app:title="@string/pref_connection_options" />
|
app:title="@string/pref_connection_options" />
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_archive_24dp"
|
android:icon="@drawable/ic_archive_24dp"
|
||||||
android:key="create_backup"
|
android:key="backup"
|
||||||
android:summary="@string/pref_create_backup_summary"
|
app:fragment="eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment"
|
||||||
android:title="@string/pref_create_backup" />
|
android:summary="@string/pref_backup_summary"
|
||||||
|
android:title="@string/backup" />
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_cloud_sync_24dp"
|
android:icon="@drawable/ic_cloud_sync_24dp"
|
||||||
app:fragment="eu.siacs.conversations.ui.fragment.settings.UpSettingsFragment"
|
app:fragment="eu.siacs.conversations.ui.fragment.settings.UpSettingsFragment"
|
||||||
|
|
Loading…
Reference in a new issue