diff --git a/README.md b/README.md
index b9cba68da25..1dcbd314746 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# Signal Android
+# Signal Android
-Signal is a simple, powerful, and secure messenger.
+Signal is a messaging app for simple private communication with friends.
-Signal uses your phone's data connection (WiFi/3G/4G/5G) to communicate securely. Millions of people use Signal every day for free and instantaneous communication anywhere in the world. Send and receive high-fidelity messages, participate in HD voice/video calls, and explore a growing set of new features that help you stay connected. Signal’s advanced privacy-preserving technology is always enabled, so you can focus on sharing the moments that matter with the people who matter to you.
+Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
-Currently available on the Play Store and [signal.org](https://signal.org/android/apk/).
+Currently available on the Play store and [signal.org](https://signal.org/android/apk/).
@@ -18,9 +18,14 @@ Want to live life on the bleeding edge and help out with testing?
You can subscribe to Signal Android Beta releases here:
https://play.google.com/apps/testing/org.thoughtcrime.securesms
-
+
If you're interested in a life of peace and tranquility, stick with the standard releases.
+## Contributing Translations
+Interested in helping to translate Signal? Contribute here:
+
+https://www.transifex.com/projects/p/signal-android/
+
## Contributing Code
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/main/CONTRIBUTING.md), that might answer some of your questions.
@@ -28,7 +33,37 @@ If you're new to the Signal codebase, we recommend going through our issues and
For larger changes and feature ideas, we ask that you propose it on the [unofficial Community Forum](https://community.signalusers.org) for a high-level discussion with the wider community before implementation.
## Contributing Ideas
-Have something you want to say about Signal projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
+Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
+
+## WhatsApp Data Import
+
+This is based on code contributed by Samuel Welten (https://github.com/jukefoxer/Signal-Android) and Wollwolke
+(https://github.com/Wollwolke/Signal-Android/tree/feature/wa-db-import). Thank you both for this.
+
+This fork of the Signal App provides a method to import one's WhatsApp conversations. It's currently still a pretty tedious process, but at least it's possible.
+
+### What works
+
+* Import 1-to-1 text conversation threads.
+* Import group chat conversations if a group chat with the same name is set up in the Signal App.
+* Importing images and videos messages from WhatsApp chats.
+
+### What doesn't work
+
+* Multimedia messages other than images and videos are currently not imported.
+* It's pretty slow (10 seconds per 1000 messages).
+
+### How to do it
+
+* Extract your unencrypted msgstore.db from your WhatsApp installation. There are several methods to do so. WhatsAppDump seems to offer a possibility that doesn't require rooting the device. A more detailed description of how to do so might be added here in the future.
+* Copy the msgstore.db file to the top level directory of your internal storage
+* Make an encrypted Backup of your Signal Messages using the built-in feature of the Signal App.
+* Build and install this version of the Signal App and import the encrypted Backup of your signal messages.
+* You might have to go to the app permission settings and give it the permission to manage all of the external storage.
+* Go to Backup => Import WhatsApp to start the import.
+* Be patient until it finishes.
+* If you're happy with the WhatsApp import create another encrypted backup of all Signal messages.
+* Install the original Signal app again and import the encrypted Backup.
Help
====
@@ -54,7 +89,7 @@ The form and manner of this distribution makes it eligible for export under the
## License
-Copyright 2013-2024 Signal Messenger, LLC
+Copyright 2013-2023 Signal
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c4f27935aa9..a87889d6993 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,10 +22,19 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1476
-val canonicalVersionName = "7.21.5"
+val canonicalVersionName = "7.21.5.0-JW"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
+// JW: re-added
+val abiPostFix: Map = mapOf(
+ "universal" to 0,
+ "armeabi-v7a" to 1,
+ "arm64-v8a" to 2,
+ "x86" to 3,
+ "x86_64" to 4
+)
+
val keystores: Map = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
val selectableVariants = listOf(
@@ -183,8 +192,8 @@ android {
manifestPlaceholders["mapsKey"] = "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
- buildConfigField("long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L")
- buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
+ buildConfigField("long", "BUILD_TIMESTAMP", "1000L") // JW: fixed time for reproducible builds, is not used anyway
+ buildConfigField("String", "GIT_HASH", "\"000000\"") // JW
buildConfigField("String", "SIGNAL_URL", "\"https://chat.signal.org\"")
buildConfigField("String", "STORAGE_URL", "\"https://storage.signal.org\"")
buildConfigField("String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"")
@@ -292,6 +301,7 @@ android {
getByName("release") {
isMinifyEnabled = true
proguardFiles(*buildTypes["debug"].proguardFiles.toTypedArray())
+ manifestPlaceholders["mapsKey"] = getMapsKey() // JW
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Release\"")
}
@@ -422,24 +432,17 @@ android {
outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
- if (output.baseName.contains("nightly")) {
- var tag = getCurrentGitTag()
- if (!tag.isNullOrEmpty()) {
- if (tag.startsWith("v")) {
- tag = tag.substring(1)
- }
- output.versionNameOverride = tag
- output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
- } else {
- output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
- }
- } else {
- output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
-
- if (currentHotfixVersion >= maxHotfixVersions) {
- throw AssertionError("Hotfix version is too large!")
- }
+ // JW: rewrote section
+ output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
+
+ val abiName: String = output.getFilter("ABI") ?: "universal"
+ val postFix: Int = abiPostFix[abiName]!!
+
+ if (postFix >= maxHotfixVersions) {
+ throw AssertionError("maxHotfixVersions is too large")
}
+
+ output.versionCodeOverride = canonicalVersionCode * maxHotfixVersions + postFix
}
}
@@ -485,6 +488,7 @@ dependencies {
implementation(project(":photoview"))
implementation(project(":core-ui"))
+ implementation("net.lingala.zip4j:zip4j:2.11.5") // JW: added
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.appcompat) {
version {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ba245937186..99c76eb78a7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,8 +29,9 @@
-
+
+
+
@@ -104,6 +105,10 @@
android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true">
+
+
+
@@ -681,12 +686,22 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
+
+
+
+
+
+
+
= 21) {
+ finishAndRemoveTask();
+ } else {
+ finish();
+ }
+
+ System.exit(0);
+ }
+
+ public static void exitAndRemoveFromRecentApps(Activity activity) {
+ Intent intent = new Intent(activity, ExitActivity.class);
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+
+ activity.startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ImportExportActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ImportExportActivity.java
new file mode 100644
index 00000000000..9e318873c95
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ImportExportActivity.java
@@ -0,0 +1,46 @@
+package org.thoughtcrime.securesms;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+
+public class ImportExportActivity extends PassphraseRequiredActivity {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = ImportExportActivity.class.getSimpleName();
+
+ private DynamicTheme dynamicTheme = new DynamicTheme();
+ private DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ @Override
+ protected void onPreCreate() {
+ dynamicTheme.onCreate(this);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ assert getSupportActionBar() != null;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ initFragment(android.R.id.content, new ImportExportFragment(), dynamicLanguage.getCurrentLocale());
+ }
+
+ @Override
+ public void onResume() {
+ dynamicTheme.onResume(this);
+ super.onResume();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+
+ switch (item.getItemId()) {
+ case android.R.id.home: finish(); return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ImportExportFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ImportExportFragment.java
new file mode 100644
index 00000000000..8d1fe9d7c99
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ImportExportFragment.java
@@ -0,0 +1,583 @@
+package org.thoughtcrime.securesms;
+
+import android.Manifest;
+import androidx.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.appcompat.app.AlertDialog;
+
+import android.provider.Settings;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.backup.BackupDialog;
+import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
+import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
+import org.thoughtcrime.securesms.database.NoExternalStorageException;
+import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
+import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
+import org.thoughtcrime.securesms.database.WhatsappBackupImporter;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.util.UriUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+
+public class ImportExportFragment extends Fragment {
+
+ private static final String TAG = ImportExportFragment.class.getSimpleName();
+
+ private static final int SUCCESS = 0;
+ private static final int NO_SD_CARD = 1;
+ private static final int ERROR_IO = 2;
+ private static final short CHOOSE_PLAINTEXT_IMPORT_LOCATION_REQUEST_CODE = 3;
+ private static final short CHOOSE_PLAINTEXT_EXPORT_LOCATION_REQUEST_CODE = 4;
+ private static final short CHOOSE_FULL_IMPORT_LOCATION_REQUEST_CODE = 5;
+ private static final short CHOOSE_FULL_EXPORT_LOCATION_REQUEST_CODE = 6;
+
+ private ProgressDialog progressDialog;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ View layout = inflater.inflate(R.layout.import_export_fragment, container, false);
+ View importWhatsappView = layout.findViewById(R.id.import_whatsapp_backup);
+ View importPlaintextView = layout.findViewById(R.id.import_plaintext_backup);
+ View importEncryptedView = layout.findViewById(R.id.import_encrypted_backup);
+ View exportPlaintextView = layout.findViewById(R.id.export_plaintext_backup);
+ View exportEncryptedView = layout.findViewById(R.id.export_encrypted_backup);
+
+ importWhatsappView.setOnClickListener(v -> handleImportWhatsappBackup());
+ importPlaintextView.setOnClickListener(v -> handleImportPlaintextBackup());
+ importEncryptedView.setOnClickListener(v -> handleImportEncryptedBackup());
+ exportPlaintextView.setOnClickListener(v -> handleExportPlaintextBackup());
+ exportEncryptedView.setOnClickListener(v -> handleExportEncryptedBackup());
+
+ return layout;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (progressDialog != null && progressDialog.isShowing()) {
+ progressDialog.dismiss();
+ progressDialog = null;
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
+ }
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @SuppressLint("InlinedApi")
+ private void handleImportWhatsappBackup() {
+ CheckAndGetAccessPermissionApi30();
+ if (existsWhatsAppMessageDatabase()) {
+ AlertDialog.Builder builder = ImportWhatsappDialog.getWhatsappBackupDialog(getActivity());
+ builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
+ Permissions.with(ImportExportFragment.this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied))
+ .onAllGranted(() -> new ImportWhatsappBackupTask(ImportWhatsappDialog.isImportGroups(), ImportWhatsappDialog.isAvoidDuplicates(), ImportWhatsappDialog.isImportMedia()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
+ .onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage, Toast.LENGTH_LONG).show())
+ .execute();
+ });
+ builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
+ builder.show();
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(getActivity().getString(R.string.ImportFragment_no_whatsapp_backup_found))
+ .setCancelable(false)
+ .setPositiveButton(getActivity().getString(R.string.ImportFragment_restore_ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ public class ImportWhatsappBackupTask extends AsyncTask {
+
+ private final boolean importGroups;
+ private final boolean importMedia;
+ private final boolean avoidDuplicates;
+
+ public ImportWhatsappBackupTask(boolean importGroups, boolean avoidDuplicates, boolean importMedia) {
+ this.importGroups = importGroups;
+ this.avoidDuplicates = avoidDuplicates;
+ this.importMedia = importMedia;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog = new ProgressDialog(getActivity());
+ progressDialog.setTitle(getActivity().getString(R.string.ImportFragment_importing));
+ progressDialog.setMessage(getActivity().getString(R.string.ImportFragment_import_whatsapp_backup_elipse));
+ progressDialog.setCancelable(false);
+ progressDialog.setIndeterminate(false);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.show();
+ }
+
+ protected void onPostExecute(Integer result) {
+ Context context = getActivity();
+
+ if (progressDialog != null)
+ progressDialog.dismiss();
+
+ if (context == null)
+ return;
+
+ switch (result) {
+ case NO_SD_CARD:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_no_whatsapp_backup_found),
+ Toast.LENGTH_LONG).show();
+ break;
+ case ERROR_IO:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_error_importing_backup),
+ Toast.LENGTH_LONG).show();
+ break;
+ case SUCCESS:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_import_complete),
+ Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ WhatsappBackupImporter.importWhatsappFromSd(getActivity(), progressDialog, importGroups, avoidDuplicates, importMedia);
+ return SUCCESS;
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, e);
+ return NO_SD_CARD;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return ERROR_IO;
+ }
+ }
+ }
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @SuppressLint("InlinedApi")
+ private void handleImportPlaintextBackup() {
+ CheckAndGetAccessPermissionApi30();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(R.drawable.symbol_error_triangle_fill_24);
+ builder.setTitle(getActivity().getString(R.string.ImportFragment_import_plaintext_backup));
+ builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_a_plaintext_backup));
+ builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
+ if (Build.VERSION.SDK_INT >= 30 && SignalStore.settings().getSignalBackupDirectory() == null ) {
+ BackupDialog.showChooseBackupLocationDialog(this, CHOOSE_PLAINTEXT_IMPORT_LOCATION_REQUEST_CODE);
+ } else if (Build.VERSION.SDK_INT < 29) {
+ Permissions.with(ImportExportFragment.this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied))
+ .onAllGranted(() ->
+ new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
+ .onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage, Toast.LENGTH_LONG).show())
+ .execute();
+ } else {
+ new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+ builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
+ builder.show();
+ }
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @SuppressLint("InlinedApi")
+ private void handleExportPlaintextBackup() {
+ CheckAndGetAccessPermissionApi30();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(R.drawable.symbol_error_triangle_fill_24);
+ builder.setTitle(getActivity().getString(R.string.ExportFragment_export_plaintext_to_storage));
+ builder.setMessage(getActivity().getString(R.string.ExportFragment_warning_this_will_export_the_plaintext_contents));
+ builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), (dialog, which) -> {
+ if (Build.VERSION.SDK_INT >= 30 && SignalStore.settings().getSignalBackupDirectory() == null ) {
+ BackupDialog.showChooseBackupLocationDialog(this, CHOOSE_PLAINTEXT_EXPORT_LOCATION_REQUEST_CODE);
+ } else if (Build.VERSION.SDK_INT < 29) {
+ Permissions.with(ImportExportFragment.this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
+ .onAllGranted(() -> new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
+ .onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage, Toast.LENGTH_LONG).show())
+ .execute();
+ } else {
+ new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+ builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
+ builder.show();
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class ImportPlaintextBackupTask extends AsyncTask {
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog = ProgressDialog.show(getActivity(),
+ getActivity().getString(R.string.ImportFragment_importing),
+ getActivity().getString(R.string.ImportFragment_import_plaintext_backup_elipse),
+ true, false);
+ }
+
+ protected void onPostExecute(Integer result) {
+ Context context = getActivity();
+
+ if (progressDialog != null)
+ progressDialog.dismiss();
+
+ if (context == null)
+ return;
+
+ switch (result) {
+ case NO_SD_CARD:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_no_plaintext_backup_found),
+ Toast.LENGTH_LONG).show();
+ break;
+ case ERROR_IO:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_error_importing_backup),
+ Toast.LENGTH_LONG).show();
+ break;
+ case SUCCESS:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_import_complete),
+ Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ PlaintextBackupImporter.importPlaintextFromSd(getActivity());
+ return SUCCESS;
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, e);
+ return NO_SD_CARD;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return ERROR_IO;
+ }
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class ExportPlaintextTask extends AsyncTask {
+ private ProgressDialog dialog;
+
+ @Override
+ protected void onPreExecute() {
+ dialog = ProgressDialog.show(getActivity(),
+ getActivity().getString(R.string.ExportFragment_exporting),
+ getActivity().getString(R.string.ExportFragment_exporting_plaintext_to_storage),
+ true, false);
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ PlaintextBackupExporter.exportPlaintextToSd(getActivity());
+ return SUCCESS;
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, e);
+ return NO_SD_CARD;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return ERROR_IO;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ Context context = getActivity();
+
+ if (dialog != null)
+ dialog.dismiss();
+
+ if (context == null)
+ return;
+
+ switch (result) {
+ case NO_SD_CARD:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_error_unable_to_write_to_storage),
+ Toast.LENGTH_LONG).show();
+ break;
+ case ERROR_IO:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_error_while_writing_to_storage),
+ Toast.LENGTH_LONG).show();
+ break;
+ case SUCCESS:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_export_successful),
+ Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+ }
+
+ public void handleImportEncryptedBackup() {
+ CheckAndGetAccessPermissionApi30();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(R.drawable.symbol_error_triangle_fill_24);
+ builder.setTitle(getActivity().getString(R.string.ImportFragment_restore_encrypted_backup));
+ builder.setMessage(getActivity().getString(R.string.ImportFragment_restoring_an_encrypted_backup_will_completely_replace_your_existing_keys));
+ builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_restore), (dialog, which) -> {
+ if (Build.VERSION.SDK_INT >= 30 && SignalStore.settings().getSignalBackupDirectory() == null ) {
+ BackupDialog.showChooseBackupLocationDialog(this, CHOOSE_FULL_IMPORT_LOCATION_REQUEST_CODE);
+ } else if (Build.VERSION.SDK_INT < 29) {
+ Permissions.with(ImportExportFragment.this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied))
+ .onAllGranted(() -> new ImportEncryptedBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
+ .onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage, Toast.LENGTH_LONG).show())
+ .execute();
+ } else {
+ new ImportEncryptedBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+ builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
+ builder.show();
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class ImportEncryptedBackupTask extends AsyncTask {
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog = ProgressDialog.show(getActivity(),
+ getActivity().getString(R.string.ImportFragment_restoring),
+ getActivity().getString(R.string.ImportFragment_restoring_encrypted_backup),
+ true, false);
+ }
+
+ protected void onPostExecute(Integer result) {
+ Context context = getActivity();
+
+ if (progressDialog != null)
+ progressDialog.dismiss();
+
+ if (context == null)
+ return;
+
+ switch (result) {
+ case NO_SD_CARD:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_no_encrypted_backup_found),
+ Toast.LENGTH_LONG).show();
+ break;
+ case ERROR_IO:
+ Toast.makeText(context,
+ context.getString(R.string.ImportFragment_error_importing_backup),
+ Toast.LENGTH_LONG).show();
+ break;
+ case SUCCESS:
+ //DatabaseFactory.getInstance(context).reset(context);
+ //Intent intent = new Intent(context, KeyCachingService.class);
+ //intent.setAction(KeyCachingService.CLEAR_KEY_ACTION);
+ //context.startService(intent);
+
+ // JW: Restart after OK press
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(context.getString(R.string.ImportFragment_restore_complete))
+ .setCancelable(false)
+ .setPositiveButton(context.getString(R.string.ImportFragment_restore_ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ ExitActivity.exitAndRemoveFromRecentApps(getActivity());
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ EncryptedBackupExporter.importFromSd(getActivity());
+ return SUCCESS;
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, e);
+ return NO_SD_CARD;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return ERROR_IO;
+ }
+ }
+ }
+
+ private void handleExportEncryptedBackup() {
+ CheckAndGetAccessPermissionApi30();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(R.drawable.symbol_info_24);
+ builder.setTitle(getActivity().getString(R.string.ExportFragment_export_to_sd_card));
+ builder.setMessage(getActivity().getString(R.string.ExportFragment_this_will_export_your_encrypted_keys_settings_and_messages));
+ builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), (dialog, which) -> {
+ if (Build.VERSION.SDK_INT >= 30 && SignalStore.settings().getSignalBackupDirectory() == null ) {
+ BackupDialog.showChooseBackupLocationDialog(this, CHOOSE_FULL_EXPORT_LOCATION_REQUEST_CODE);
+ } else if (Build.VERSION.SDK_INT < 29) {
+ Permissions.with(ImportExportFragment.this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
+ .onAllGranted(() -> new ExportEncryptedTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
+ .onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage, Toast.LENGTH_LONG).show())
+ .execute();
+ } else {
+ new ExportEncryptedTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+ builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
+ builder.show();
+ }
+
+ private class ExportEncryptedTask extends AsyncTask {
+ private ProgressDialog dialog;
+
+ @Override
+ protected void onPreExecute() {
+ dialog = ProgressDialog.show(getActivity(),
+ getActivity().getString(R.string.ExportFragment_exporting),
+ getActivity().getString(R.string.ExportFragment_exporting_keys_settings_and_messages),
+ true, false);
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ Context context = getActivity();
+
+ if (dialog != null) dialog.dismiss();
+
+ if (context == null) return;
+
+ switch (result) {
+ case NO_SD_CARD:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_error_unable_to_write_to_storage),
+ Toast.LENGTH_LONG).show();
+ break;
+ case ERROR_IO:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_error_while_writing_to_storage),
+ Toast.LENGTH_LONG).show();
+ break;
+ case SUCCESS:
+ Toast.makeText(context,
+ context.getString(R.string.ExportFragment_export_successful),
+ Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ EncryptedBackupExporter.exportToSd(getActivity());
+ return SUCCESS;
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, e);
+ return NO_SD_CARD;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return ERROR_IO;
+ }
+ }
+ }
+
+ private boolean existsWhatsAppMessageDatabase() {
+ String dbfile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "msgstore.db";
+ File msgdb = new File(dbfile);
+ if (msgdb.exists()) return true; else return false;
+ }
+
+ private void CheckAndGetAccessPermissionApi30() {
+ if (Build.VERSION.SDK_INT >= 30) {
+ if (!Environment.isExternalStorageManager()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(getActivity().getString(R.string.ImportExportFragment_signal_needs_the_all_files_access_permission))
+ .setCancelable(false)
+ .setPositiveButton(getActivity().getString(R.string.ImportFragment_restore_ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ startActivity(intent);
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data != null && data.getData() != null) {
+ Uri backupDirectoryUri = data.getData();
+ SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
+ String backupDir = UriUtils.getFullPathFromTreeUri(getActivity(), backupDirectoryUri);
+ int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+
+ Log.w(TAG, "Take permission from Uri: " + backupDir);
+ getActivity().getContentResolver().takePersistableUriPermission(backupDirectoryUri, takeFlags);
+
+ if (resultCode == Activity.RESULT_OK) {
+ switch (requestCode) {
+ case CHOOSE_PLAINTEXT_IMPORT_LOCATION_REQUEST_CODE:
+ new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ case CHOOSE_PLAINTEXT_EXPORT_LOCATION_REQUEST_CODE:
+ new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ case CHOOSE_FULL_IMPORT_LOCATION_REQUEST_CODE:
+ new ImportEncryptedBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ case CHOOSE_FULL_EXPORT_LOCATION_REQUEST_CODE:
+ new ExportEncryptedTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ImportWhatsappDialog.java b/app/src/main/java/org/thoughtcrime/securesms/ImportWhatsappDialog.java
new file mode 100644
index 00000000000..d7bbbace710
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ImportWhatsappDialog.java
@@ -0,0 +1,57 @@
+package org.thoughtcrime.securesms;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.view.View;
+import android.widget.CheckBox;
+
+import androidx.appcompat.app.AlertDialog;
+
+public class ImportWhatsappDialog {
+
+ private static boolean importGroups = false;
+ private static boolean avoidDuplicates = false;
+ private static boolean importMedia = false;
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @SuppressLint("InlinedApi")
+ public static AlertDialog.Builder getWhatsappBackupDialog(Activity activity) {
+ View checkBoxView = View.inflate(activity, R.layout.dialog_import_whatsapp, null);
+ CheckBox importGroupsCheckbox = checkBoxView.findViewById(R.id.import_groups_checkbox);
+ importGroupsCheckbox.setChecked(importGroups);
+ importGroupsCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ ImportWhatsappDialog.importGroups = isChecked;
+ });
+
+ CheckBox avoidDuplicatesCheckbox = checkBoxView.findViewById(R.id.avoid_duplicates_checkbox);
+ avoidDuplicatesCheckbox.setChecked(avoidDuplicates);
+ avoidDuplicatesCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ ImportWhatsappDialog.avoidDuplicates = isChecked;
+ });
+
+ CheckBox importMediaCheckbox = checkBoxView.findViewById(R.id.import_media_checkbox);
+ importMediaCheckbox.setChecked(importMedia);
+ importMediaCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ ImportWhatsappDialog.importMedia = isChecked;
+ });
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setIcon(R.drawable.symbol_error_triangle_fill_24);
+ builder.setTitle(activity.getString(R.string.ImportFragment_import_whatsapp_backup));
+ builder.setMessage(activity.getString(R.string.ImportFragment_this_will_import_messages_from_whatsapp_backup))
+ .setView(checkBoxView);
+ return builder;
+ }
+
+ public static boolean isImportGroups() {
+ return importGroups;
+ }
+
+ public static boolean isAvoidDuplicates() {
+ return avoidDuplicates;
+ }
+
+ public static boolean isImportMedia() {
+ return importMedia;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
index de3387945c5..0616a51df43 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
@@ -33,6 +33,7 @@
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.WindowManager; // JW: added
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
@@ -271,10 +272,13 @@ private void initializeResources() {
private void setLockTypeVisibility() {
if (SignalStore.settings().getScreenLockEnabled()) {
passphraseAuthContainer.setVisibility(View.GONE);
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); // JW: to override setSoftInputMode it has to be defined in the manifest file
unlockView.setVisibility(View.VISIBLE);
lockScreenButton.setVisibility(View.VISIBLE);
} else {
passphraseAuthContainer.setVisibility(View.VISIBLE);
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); // JW
+ passphraseText.requestFocus(); // JW
unlockView.setVisibility(View.GONE);
lockScreenButton.setVisibility(View.GONE);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/location/SignalMapView.java b/app/src/main/java/org/thoughtcrime/securesms/components/location/SignalMapView.java
index 00156d5c861..53f76639d4a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/location/SignalMapView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/location/SignalMapView.java
@@ -20,6 +20,7 @@
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.TextSecurePreferences; // JW: added
import java.util.concurrent.ExecutionException;
@@ -28,6 +29,7 @@ public class SignalMapView extends LinearLayout {
private MapView mapView;
private ImageView imageView;
private TextView textView;
+ public static int mapType; // JW
public SignalMapView(Context context) {
this(context, null);
@@ -50,6 +52,18 @@ private void initialize(Context context) {
this.mapView = findViewById(R.id.map_view);
this.imageView = findViewById(R.id.image_view);
this.textView = findViewById(R.id.address_view);
+ this.mapType = getGoogleMapType(context); // JW
+ }
+
+ // JW: get the maptype
+ private int getGoogleMapType(Context context) {
+ switch (TextSecurePreferences.getGoogleMapType(context)) {
+ case "hybrid": return GoogleMap.MAP_TYPE_HYBRID;
+ case "satellite": return GoogleMap.MAP_TYPE_SATELLITE;
+ case "terrain": return GoogleMap.MAP_TYPE_TERRAIN;
+ case "none": return GoogleMap.MAP_TYPE_NONE;
+ default: return GoogleMap.MAP_TYPE_NORMAL;
+ }
}
public ListenableFuture display(final SignalPlace place) {
@@ -86,7 +100,7 @@ public static ListenableFuture snapshot(final LatLng place, @NonNull fin
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place, 13));
googleMap.addMarker(new MarkerOptions().position(place));
googleMap.setBuildingsEnabled(true);
- googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
+ googleMap.setMapType(mapType); // JW: set maptype
googleMap.getUiSettings().setAllGesturesEnabled(false);
googleMap.setOnMapLoadedCallback(() -> googleMap.snapshot(bitmap -> {
future.set(bitmap);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
index 1678d5fc894..44e45bb001d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
@@ -1,19 +1,32 @@
package org.thoughtcrime.securesms.components.settings.app.chats
+import android.app.Activity // JW: added
+import android.content.Intent // JW: added
+import android.os.Build // JW: added
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.backup.BackupDialog // JW: added
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
+import org.thoughtcrime.securesms.service.LocalBackupListener // JW: added
+import org.thoughtcrime.securesms.util.TextSecurePreferences // JW: added
+import org.thoughtcrime.securesms.keyvalue.SignalStore // JW: added
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
+import org.thoughtcrime.securesms.util.UriUtils // JW: added
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
private lateinit var viewModel: ChatsSettingsViewModel
+ private val mapLabels by lazy { resources.getStringArray(R.array.pref_map_type_entries) } // JW: added
+ private val mapValues by lazy { resources.getStringArray(R.array.pref_map_type_values) } // JW: added
+ private val groupAddLabels by lazy { resources.getStringArray(R.array.pref_group_add_entries) } // JW: added
+ private val groupAddValues by lazy { resources.getStringArray(R.array.pref_group_add_values) } // JW: added
+ val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 1201 // JW: added
override fun onResume() {
super.onResume()
@@ -29,6 +42,25 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
}
}
+ // JW: added
+ override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+ super.onActivityResult(requestCode, resultCode, intent)
+
+ if (intent != null && intent.data != null) {
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE) {
+ val backupUri = intent.data
+ val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ SignalStore.settings.setSignalBackupDirectory(backupUri!!)
+ context?.getContentResolver()?.takePersistableUriPermission(backupUri, takeFlags)
+ TextSecurePreferences.setNextBackupTime(requireContext(), 0)
+ LocalBackupListener.schedule(context)
+ viewModel.setChatBackupLocationApi30(UriUtils.getFullPathFromTreeUri(context, backupUri))
+ }
+ }
+ }
+ }
+
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
return configure {
switchPref(
@@ -91,7 +123,8 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
}
)
- if (!RemoteConfig.messageBackups) {
+ //if (!RemoteConfig.messageBackups) {
+ if (true) { // JW: always enable local backups
dividerPref()
sectionHeaderPref(R.string.preferences_chats__backups)
@@ -103,6 +136,112 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}
)
+
+ // JW: added
+ if (Build.VERSION.SDK_INT < 30) {
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_backups_removable),
+ summary = DSLSettingsText.from(R.string.preferences_chats__backup_chats_to_removable_storage),
+ isChecked = state.chatBackupsLocation,
+ onClick = {
+ viewModel.setChatBackupLocation(!state.chatBackupsLocation)
+ }
+ )
+ } else {
+ val backupUri = SignalStore.settings.signalBackupDirectory
+ val summaryText = UriUtils.getFullPathFromTreeUri(context, backupUri)
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_backups_location_tap_to_change),
+ summary = DSLSettingsText.from(summaryText),
+ onClick = {
+ BackupDialog.showChooseBackupLocationDialog(this@ChatsSettingsFragment, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE)
+ viewModel.setChatBackupLocationApi30(UriUtils.getFullPathFromTreeUri(context, backupUri))
+ }
+ )
+ }
+
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_backups_zipfile),
+ summary = DSLSettingsText.from(R.string.preferences_chats__backup_chats_to_encrypted_zipfile),
+ isChecked = state.chatBackupZipfile,
+ onClick = {
+ viewModel.setChatBackupZipfile(!state.chatBackupZipfile)
+ }
+ )
+
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_backups_zipfile_plain),
+ summary = DSLSettingsText.from(R.string.preferences_chats__backup_chats_to_encrypted_zipfile_plain),
+ isChecked = state.chatBackupZipfilePlain,
+ onClick = {
+ viewModel.setChatBackupZipfilePlain(!state.chatBackupZipfilePlain)
+ }
+ )
+
+ dividerPref()
+
+ sectionHeaderPref(R.string.preferences_chats__control_message_deletion)
+
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_keep_view_once_messages),
+ summary = DSLSettingsText.from(R.string.preferences_chats__keep_view_once_messages_summary),
+ isChecked = state.keepViewOnceMessages,
+ onClick = {
+ viewModel.keepViewOnceMessages(!state.keepViewOnceMessages)
+ }
+ )
+
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__chat_ignore_remote_delete),
+ summary = DSLSettingsText.from(R.string.preferences_chats__chat_ignore_remote_delete_summary),
+ isChecked = state.ignoreRemoteDelete,
+ onClick = {
+ viewModel.ignoreRemoteDelete(!state.ignoreRemoteDelete)
+ }
+ )
+
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__delete_media_only),
+ summary = DSLSettingsText.from(R.string.preferences_chats__delete_media_only_summary),
+ isChecked = state.deleteMediaOnly,
+ onClick = {
+ viewModel.deleteMediaOnly(!state.deleteMediaOnly)
+ }
+ )
+
+ dividerPref()
+
+ sectionHeaderPref(R.string.preferences_chats__group_control)
+
+ // JW: added
+ radioListPref(
+ title = DSLSettingsText.from(R.string.preferences_chats__who_can_add_you_to_groups),
+ listItems = groupAddLabels,
+ selected = groupAddValues.indexOf(state.whoCanAddYouToGroups),
+ onSelected = {
+ viewModel.setWhoCanAddYouToGroups(groupAddValues[it])
+ }
+ )
+
+ dividerPref()
+
+ sectionHeaderPref(R.string.preferences_chats__google_map_type)
+
+ // JW: added
+ radioListPref(
+ title = DSLSettingsText.from(R.string.preferences__map_type),
+ listItems = mapLabels,
+ selected = mapValues.indexOf(state.googleMapType),
+ onSelected = {
+ viewModel.setGoogleMapType(mapValues[it])
+ }
+ )
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
index ed174592c20..4d478278347 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
@@ -7,4 +7,15 @@ data class ChatsSettingsState(
val useSystemEmoji: Boolean,
val enterKeySends: Boolean,
val localBackupsEnabled: Boolean
+ // JW: added extra preferences
+ ,
+ val chatBackupsLocation: Boolean,
+ val chatBackupsLocationApi30: String,
+ val chatBackupZipfile: Boolean,
+ val chatBackupZipfilePlain: Boolean,
+ val keepViewOnceMessages: Boolean,
+ val ignoreRemoteDelete: Boolean,
+ val deleteMediaOnly: Boolean,
+ val googleMapType: String,
+ val whoCanAddYouToGroups: String
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
index f7267380637..0dd02e2ce4f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
@@ -6,7 +6,9 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.ConversationUtil
+import org.thoughtcrime.securesms.util.TextSecurePreferences // JW: added
import org.thoughtcrime.securesms.util.ThrottledDebouncer
+import org.thoughtcrime.securesms.util.UriUtils // JW: added
import org.thoughtcrime.securesms.util.livedata.Store
class ChatsSettingsViewModel @JvmOverloads constructor(
@@ -23,6 +25,17 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
useSystemEmoji = SignalStore.settings.isPreferSystemEmoji,
enterKeySends = SignalStore.settings.isEnterKeySends,
localBackupsEnabled = SignalStore.settings.isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(AppDependencies.application)
+ // JW: added
+ ,
+ chatBackupsLocation = TextSecurePreferences.isBackupLocationRemovable(AppDependencies.application),
+ chatBackupsLocationApi30 = UriUtils.getFullPathFromTreeUri(AppDependencies.application, SignalStore.settings.signalBackupDirectory),
+ chatBackupZipfile = TextSecurePreferences.isRawBackupInZipfile(AppDependencies.application),
+ chatBackupZipfilePlain = TextSecurePreferences.isPlainBackupInZipfile(AppDependencies.application),
+ keepViewOnceMessages = TextSecurePreferences.isKeepViewOnceMessages(AppDependencies.application),
+ ignoreRemoteDelete = TextSecurePreferences.isIgnoreRemoteDelete(AppDependencies.application),
+ deleteMediaOnly = TextSecurePreferences.isDeleteMediaOnly(AppDependencies.application),
+ googleMapType = TextSecurePreferences.getGoogleMapType(AppDependencies.application),
+ whoCanAddYouToGroups = TextSecurePreferences.whoCanAddYouToGroups(AppDependencies.application)
)
)
@@ -64,5 +77,80 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
) {
store.update { it.copy(localBackupsEnabled = backupsEnabled) }
}
+ // JW: added. This is required to update the UI for settings that are not in the Signal store but in the shared preferences.
+ store.update { getState().copy() }
}
+
+ // JW: added
+ fun setChatBackupLocation(enabled: Boolean) {
+ TextSecurePreferences.setBackupLocationRemovable(AppDependencies.application, enabled)
+ TextSecurePreferences.setBackupLocationChanged(AppDependencies.application, true) // Used in BackupUtil.getAllBackupsNewestFirst()
+ refresh()
+ }
+
+ // JW: added
+ fun setChatBackupLocationApi30(value: String) {
+ refresh()
+ }
+
+ // JW: added
+ fun setChatBackupZipfile(enabled: Boolean) {
+ TextSecurePreferences.setRawBackupZipfile(AppDependencies.application, enabled)
+ refresh()
+ }
+
+ // JW: added
+ fun setChatBackupZipfilePlain(enabled: Boolean) {
+ TextSecurePreferences.setPlainBackupZipfile(AppDependencies.application, enabled)
+ refresh()
+ }
+
+ // JW: added
+ fun keepViewOnceMessages(enabled: Boolean) {
+ TextSecurePreferences.setKeepViewOnceMessages(AppDependencies.application, enabled)
+ refresh()
+ }
+
+ // JW: added
+ fun ignoreRemoteDelete(enabled: Boolean) {
+ TextSecurePreferences.setIgnoreRemoteDelete(AppDependencies.application, enabled)
+ refresh()
+ }
+
+ // JW: added
+ fun deleteMediaOnly(enabled: Boolean) {
+ TextSecurePreferences.setDeleteMediaOnly(AppDependencies.application, enabled)
+ refresh()
+ }
+
+ // JW: added
+ fun setGoogleMapType(mapType: String) {
+ TextSecurePreferences.setGoogleMapType(AppDependencies.application, mapType)
+ refresh()
+ }
+
+ // JW: added
+ fun setWhoCanAddYouToGroups(adder: String) {
+ TextSecurePreferences.setWhoCanAddYouToGroups(AppDependencies.application, adder)
+ refresh()
+ }
+
+ // JW: added
+ private fun getState() = ChatsSettingsState(
+ generateLinkPreviews = SignalStore.settings.isLinkPreviewsEnabled,
+ useAddressBook = SignalStore.settings.isPreferSystemContactPhotos,
+ keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived(),
+ useSystemEmoji = SignalStore.settings.isPreferSystemEmoji,
+ enterKeySends = SignalStore.settings.isEnterKeySends,
+ localBackupsEnabled = SignalStore.settings.isBackupEnabled,
+ chatBackupsLocationApi30 = UriUtils.getFullPathFromTreeUri(AppDependencies.application, SignalStore.settings.signalBackupDirectory),
+ chatBackupsLocation = TextSecurePreferences.isBackupLocationRemovable(AppDependencies.application),
+ chatBackupZipfile = TextSecurePreferences.isRawBackupInZipfile(AppDependencies.application),
+ chatBackupZipfilePlain = TextSecurePreferences.isPlainBackupInZipfile(AppDependencies.application),
+ keepViewOnceMessages = TextSecurePreferences.isKeepViewOnceMessages(AppDependencies.application),
+ ignoreRemoteDelete = TextSecurePreferences.isIgnoreRemoteDelete(AppDependencies.application),
+ deleteMediaOnly = TextSecurePreferences.isDeleteMediaOnly(AppDependencies.application),
+ googleMapType = TextSecurePreferences.getGoogleMapType(AppDependencies.application),
+ whoCanAddYouToGroups = TextSecurePreferences.whoCanAddYouToGroups(AppDependencies.application)
+ )
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
index d45ccde7e38..abd0138f9a9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
@@ -182,30 +182,62 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
sectionHeaderPref(R.string.PrivacySettingsFragment__app_security)
- if (state.isObsoletePasswordEnabled) {
+ // JW: added toggle between password and Android screenlock
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_app_protection__method_passphrase),
+ summary = DSLSettingsText.from(R.string.preferences_app_protection__method_passphrase_summary),
+ isChecked = state.isProtectionMethodPassphrase,
+ onClick = {
+ // After a togggle, we disable both passphrase and Android keylock.
+ // Remove the passphrase if there is one set
+ if (state.isObsoletePasswordEnabled) {
+ MasterSecretUtil.changeMasterSecretPassphrase(
+ activity,
+ KeyCachingService.getMasterSecret(context),
+ MasterSecretUtil.UNENCRYPTED_PASSPHRASE
+ )
+ SignalStore.settings.setPassphraseDisabled(true)
+ val intent = Intent(context, KeyCachingService::class.java)
+ intent.action = KeyCachingService.DISABLE_ACTION
+ requireActivity().startService(intent)
+ }
+ TextSecurePreferences.setProtectionMethod(activity, !state.isProtectionMethodPassphrase)
+ viewModel.setNoLock()
+ }
+ )
+
+ //if (state.isObsoletePasswordEnabled) {
+ if (viewModel.isPassphraseSelected()) { // JW: method changed
switchPref(
title = DSLSettingsText.from(R.string.preferences__enable_passphrase),
summary = DSLSettingsText.from(R.string.preferences__lock_signal_and_message_notifications_with_a_passphrase),
- isChecked = true,
+ isChecked = state.isObsoletePasswordEnabled, // JW
onClick = {
- MaterialAlertDialogBuilder(requireContext()).apply {
- setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
- setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
- setIcon(R.drawable.symbol_error_triangle_fill_24)
- setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { _, _ ->
- MasterSecretUtil.changeMasterSecretPassphrase(
- activity,
- KeyCachingService.getMasterSecret(context),
- MasterSecretUtil.UNENCRYPTED_PASSPHRASE
- )
- SignalStore.settings.passphraseDisabled = true
- val intent = Intent(activity, KeyCachingService::class.java)
- intent.action = KeyCachingService.DISABLE_ACTION
- requireActivity().startService(intent)
- viewModel.refresh()
+ if (state.isObsoletePasswordEnabled) { // JW: added if else
+ MaterialAlertDialogBuilder(requireContext()).apply {
+ setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
+ setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
+ setIcon(R.drawable.symbol_error_triangle_fill_24)
+ setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { _, _ ->
+ MasterSecretUtil.changeMasterSecretPassphrase(
+ activity,
+ KeyCachingService.getMasterSecret(context),
+ MasterSecretUtil.UNENCRYPTED_PASSPHRASE
+ )
+ SignalStore.settings.setPassphraseDisabled(true)
+ val intent = Intent(activity, KeyCachingService::class.java)
+ intent.action = KeyCachingService.DISABLE_ACTION
+ requireActivity().startService(intent)
+ viewModel.refresh()
+ }
+ setNegativeButton(android.R.string.cancel, null)
+ show()
}
- setNegativeButton(android.R.string.cancel, null)
- show()
+ } else {
+ // enable password
+ val intent = Intent(activity, PassphraseChangeActivity::class.java)
+ startActivity(intent)
+ viewModel.refresh()
}
}
)
@@ -237,6 +269,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
clickPref(
title = DSLSettingsText.from(R.string.preferences__inactivity_timeout_interval),
+ summary = DSLSettingsText.from(getScreenLockInactivityTimeoutSummary(state.isObsoletePasswordTimeoutEnabled,60 * state.obsoletePasswordTimeout.toLong())), // JW
onClick = {
childFragmentManager.clearFragmentResult(TimeDurationPickerDialog.RESULT_DURATION)
childFragmentManager.clearFragmentResultListener(TimeDurationPickerDialog.RESULT_DURATION)
@@ -244,7 +277,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
val timeout = bundle.getLong(TimeDurationPickerDialog.RESULT_KEY_DURATION_MILLISECONDS).milliseconds.inWholeMinutes.toInt()
viewModel.setObsoletePasswordTimeout(max(timeout, 1))
}
- TimeDurationPickerDialog.create(state.screenLockActivityTimeout.seconds).show(childFragmentManager, null)
+ TimeDurationPickerDialog.create(state.obsoletePasswordTimeout.seconds * 60).show(childFragmentManager, null) // JW
}
)
} else {
@@ -258,6 +291,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
},
isEnabled = isKeyguardSecure,
onDisabledClicked = {
+ viewModel.setOnlyScreenlockEnabled(!state.screenLock) // JW: changed
Snackbar
.make(
requireView(),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
index 06c314db15f..08c7815ea6b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
@@ -13,4 +13,7 @@ data class PrivacySettingsState(
val isObsoletePasswordTimeoutEnabled: Boolean,
val obsoletePasswordTimeout: Int,
val universalExpireTimer: Int
+ // JW: added
+ ,
+ val isProtectionMethodPassphrase: Boolean
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
index 408c988a4b4..d7b9a197d07 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
@@ -62,6 +62,40 @@ class PrivacySettingsViewModel(
refresh()
}
+ // JW: added
+ fun setPassphraseEnabled(enabled: Boolean) {
+ SignalStore.settings.setPassphraseDisabled(!enabled)
+ SignalStore.settings.setScreenLockEnabled(!enabled)
+ sharedPreferences.edit().putBoolean("pref_enable_passphrase_temporary", enabled).apply()
+ refresh()
+ }
+
+ // JW: added
+ fun setOnlyScreenlockEnabled(enabled: Boolean) {
+ SignalStore.settings.setPassphraseDisabled(true)
+ SignalStore.settings.setScreenLockEnabled(enabled)
+ sharedPreferences.edit().putBoolean("pref_enable_passphrase_temporary", false).apply()
+ refresh()
+ }
+
+ // JW: added
+ fun setNoLock() {
+ SignalStore.settings.setPassphraseDisabled(true)
+ SignalStore.settings.setScreenLockEnabled(false)
+ sharedPreferences.edit().putBoolean("pref_enable_passphrase_temporary", false).apply()
+ refresh()
+ }
+
+ // JW: added method.
+ fun isPassphraseSelected(): Boolean {
+ // Because this preference may be undefined when this app is first ran we also check if there is a passphrase
+ // defined, if so, we assume passphrase protection:
+ val myContext = AppDependencies.application
+
+ return TextSecurePreferences.isProtectionMethodPassphrase(myContext) ||
+ TextSecurePreferences.getBooleanPreference(myContext, "pref_enable_passphrase_temporary", false) && !SignalStore.settings.getPassphraseDisabled()
+ }
+
fun refresh() {
store.update(this::updateState)
}
@@ -80,6 +114,9 @@ class PrivacySettingsViewModel(
isObsoletePasswordTimeoutEnabled = SignalStore.settings.passphraseTimeoutEnabled,
obsoletePasswordTimeout = SignalStore.settings.passphraseTimeout,
universalExpireTimer = SignalStore.settings.universalExpireTimer
+ // JW: added
+ ,
+ isProtectionMethodPassphrase = TextSecurePreferences.isProtectionMethodPassphrase(AppDependencies.application)
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt
index 9d0dd5052d9..d7b02148880 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt
@@ -110,6 +110,17 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
viewModel.setAlwaysRelayCalls(!state.alwaysRelayCalls)
}
+ // JW: added
+ switchPref(
+ title = DSLSettingsText.from(R.string.preferences_advanced__push_notifications_fcm),
+ summary = DSLSettingsText.from(R.string.preferences_advanced__push_notifications_fcm_summary),
+ isChecked = state.pushNotificationsViaFCM
+ ) {
+ val isFcm = state.pushNotificationsViaFCM
+ viewModel.setPushNotificationsViaFCM(!isFcm)
+ FCMPreferenceFunctions.onFCMPreferenceChange(context, !isFcm)
+ }
+
dividerPref()
sectionHeaderPref(R.string.preferences_communication__category_censorship_circumvention)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt
index 3f5692d2b72..151da675f8c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt
@@ -8,6 +8,9 @@ data class AdvancedPrivacySettingsState(
val showSealedSenderStatusIcon: Boolean,
val allowSealedSenderFromAnyone: Boolean,
val showProgressSpinner: Boolean
+ // JW: added
+ ,
+ val pushNotificationsViaFCM: Boolean
)
enum class CensorshipCircumventionState(val available: Boolean) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt
index 9dff6b96457..352a6e64738 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt
@@ -62,6 +62,12 @@ class AdvancedPrivacySettingsViewModel(
refresh()
}
+ // JW: added
+ fun setPushNotificationsViaFCM(enabled: Boolean) {
+ SignalStore.account.fcmEnabled = enabled
+ refresh()
+ }
+
fun refresh() {
store.update { getState().copy(showProgressSpinner = it.showProgressSpinner) }
}
@@ -85,6 +91,9 @@ class AdvancedPrivacySettingsViewModel(
AppDependencies.application
),
false
+ // JW: added
+ ,
+ pushNotificationsViaFCM = SignalStore.account.fcmEnabled
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/FCMPreferenceFunctions.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/FCMPreferenceFunctions.java
new file mode 100644
index 00000000000..35c20427671
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/FCMPreferenceFunctions.java
@@ -0,0 +1,127 @@
+package org.thoughtcrime.securesms.components.settings.app.privacy.advanced;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.dependencies.AppDependencies;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+
+import java.io.IOException;
+import java.util.Optional;
+
+//-----------------------------------------------------------------------------
+// JW: added
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager;
+import org.thoughtcrime.securesms.ExitActivity;
+import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
+import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
+import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
+import org.thoughtcrime.securesms.util.PlayServicesUtil;
+//-----------------------------------------------------------------------------
+
+public class FCMPreferenceFunctions {
+ private static final String TAG = Log.tag(FCMPreferenceFunctions.class);
+
+ public static void cleanGcmId(Context context) {
+ try {
+ SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager();
+ accountManager.setGcmId(Optional.empty());
+ } catch (IOException e) {
+ Log.w(TAG, e.getMessage());
+ Toast.makeText(context, R.string.ApplicationPreferencesActivity_error_connecting_to_server, Toast.LENGTH_LONG).show();
+ } catch (Exception e) {
+ Log.w(TAG, e.getMessage());
+ Toast.makeText(context, "Exception: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public static void exitAndRestart(Context context) {
+ // JW: Restart after OK press
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(context.getString(R.string.preferences_advanced__need_to_restart))
+ .setCancelable(false)
+ .setPositiveButton(context.getString(R.string.ImportFragment_restore_ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ restartApp(context);
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ // Create a pending intent to restart Signal
+ public static void restartApp(Context context) {
+ try {
+ if (context != null) {
+ PackageManager pm = context.getPackageManager();
+
+ if (pm != null) {
+ Intent startActivity = pm.getLaunchIntentForPackage(context.getPackageName());
+ if (startActivity != null) {
+ startActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ int pendingIntentId = 223344;
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, pendingIntentId, startActivity, PendingIntent.FLAG_CANCEL_CURRENT);
+ AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent);
+ System.exit(0);
+ //ExitActivity.exitAndRemoveFromRecentApps(getActivity());
+ } else {
+ Log.e(TAG, "restartApp: unable to restart application, startActivity == null");
+ System.exit(0);
+ }
+ } else {
+ Log.e(TAG, "restartApp: unable to restart application, Package manager == null");
+ System.exit(0);
+ }
+ } else {
+ Log.e(TAG, "restartApp: unable to restart application, Context == null");
+ System.exit(0);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "restartApp: unable to restart application: " + e.getMessage());
+ System.exit(0);
+ }
+ }
+
+ public static void onFCMPreferenceChange(Context context, boolean isFcm) {
+ if (isFcm) {
+ PlayServicesUtil.PlayServicesStatus status = PlayServicesUtil.getPlayServicesStatus(context);
+
+ if (status == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
+ SignalStore.account().setFcmEnabled(true);
+ //TextSecurePreferences.setWebsocketRegistered(context, false);
+ Toast.makeText(context, "Setting setFcmDisabled to false", Toast.LENGTH_LONG).show();
+ AppDependencies.getJobManager().startChain(new FcmRefreshJob())
+ .then(new RefreshAttributesJob())
+ .enqueue();
+
+ context.stopService(new Intent(context, IncomingMessageObserver.ForegroundService.class));
+
+ Log.i(TAG, "onFCMPreferenceChange: enabled fcm");
+ exitAndRestart(context);
+ } else {
+ // No Play Services found
+ Toast.makeText(context, R.string.preferences_advanced__play_services_not_found, Toast.LENGTH_LONG).show();
+ }
+ } else { // switch to websockets
+ SignalStore.account().setFcmEnabled(false);
+ //TextSecurePreferences.setWebsocketRegistered(context, true);
+ SignalStore.account().setFcmToken(null);
+ cleanGcmId(context);
+ AppDependencies.getJobManager().add(new RefreshAttributesJob());
+ Log.i(TAG, "onFCMPreferenceChange: disabled fcm");
+ Toast.makeText(context, "Switching to Websockets", Toast.LENGTH_LONG).show();
+ exitAndRestart(context);
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index 8bcdd683af7..923a98f7328 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -84,6 +84,7 @@
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.ImportExportActivity; // JW
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MainFragment;
import org.thoughtcrime.securesms.MainNavigator;
@@ -613,11 +614,17 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
} else if (itemId == R.id.menu_clear_unread_filter) {
onClearFilterClick();
return true;
+ } else if (itemId == R.id.menu_import_export) { handleImportExport(); return true; // JW: added
} else {
return false;
}
}
+ // JW: added
+ private void handleImportExport() {
+ startActivity(new Intent(requireActivity(), ImportExportActivity.class));
+ }
+
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java
new file mode 100644
index 00000000000..82c893d6576
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java
@@ -0,0 +1,508 @@
+/**
+ * Copyright (C) 2011 Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.database;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Environment;
+
+import androidx.annotation.NonNull;
+
+import org.signal.core.util.Base64;
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.crypto.AttachmentSecret;
+import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
+import org.thoughtcrime.securesms.backup.BackupPassphrase;
+import org.thoughtcrime.securesms.crypto.DatabaseSecret;
+import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
+import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
+import org.thoughtcrime.securesms.util.FileUtilsJW;
+import org.thoughtcrime.securesms.util.JsonUtils;
+import org.thoughtcrime.securesms.util.StorageUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.String;
+import java.nio.channels.FileChannel;
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.Set;
+
+public class EncryptedBackupExporter {
+
+ private static final String TAG = Log.tag(EncryptedBackupExporter.class);
+
+ // Files used to store the DatabaseSecret, AttachmentSecret and LogSecret, required after the transfer to SQLCipher in Signal 4.16
+ private static final String databaseSecretFile = "databasesecret.txt";
+ private static final String attachmentSecretFile = "attachmentsecret.txt";
+ private static final String logSecretFile = "logsecret.txt";
+ private static final String backupKeyFile = "backupkey.txt";
+ private static final String exportDirectory = "SignalExport";
+ private static final String secretsExportDirectory = "SignalSecrets";
+
+ public static void exportToSd(Context context) throws NoExternalStorageException, IOException {
+ verifyExternalStorageForExport(context);
+ AttachmentSecretProvider asp = AttachmentSecretProvider.getInstance(context);
+ DatabaseSecret dbs = DatabaseSecretProvider.getOrCreateDatabaseSecret(context);
+ AttachmentSecret ats = asp.getOrCreateAttachmentSecret();
+ byte[] lgs = getOrCreateLogSecret(context);
+ String bks = BackupPassphrase.get(context);
+ exportDirectory(context, "");
+ exportSecrets(context, dbs, ats, lgs, bks);
+ if (TextSecurePreferences.isRawBackupInZipfile(context)) {
+ File test = new File(getEncryptedZipfileName());
+ if (test.exists()) {
+ test.delete();
+ }
+ FileUtilsJW.createEncryptedZipfile(context, getEncryptedZipfileName(), getExportDirectoryPath(context), getExportSecretsDirectory(context));
+ deleteRawBackupFiles(context);
+ }
+ }
+
+ public static void importFromSd(Context context) throws NoExternalStorageException, IOException {
+ // Store in a boolean because settings might change after restore
+ boolean rawBackupInZipfile = TextSecurePreferences.isRawBackupInZipfile(context);
+ // Extract the zipfile
+ if (rawBackupInZipfile) {
+ FileUtilsJW.extractEncryptedZipfile(context, getEncryptedZipfileName(), StorageUtil.getRawBackupDirectory().getAbsolutePath());
+ }
+ verifyExternalStorageForImport(context);
+ importDirectory(context, "");
+ importSharedSettings(context);
+ importSecrets(context);
+ if (rawBackupInZipfile) {
+ deleteRawBackupFiles(context);
+ }
+ }
+
+ private static void importSharedSettings(Context context) {
+ String tempFileName = "tempsettings";
+ String settingsFile = "org.thoughtcrime.securesms_preferences";
+
+ File fromPrefFile = new File(getExportDirectoryPath(context) + File.separator + "shared_prefs" + File.separator + settingsFile + ".xml");
+ // Copy fromFile to shared_prefs
+ File toTempFile = new File(context.getFilesDir() + File.separator + ".." + File.separator + "shared_prefs" + File.separator + tempFileName + ".xml");
+ migrateFile(fromPrefFile, toTempFile);
+ // Get default preferences
+ SharedPreferences defaultPreferences = context.getSharedPreferences(settingsFile, Context.MODE_PRIVATE);
+ // Load settings from backup file
+ SharedPreferences newPreferences = context.getSharedPreferences(tempFileName, Context.MODE_PRIVATE);
+ // Write settings to current file
+ copySharedPreferences(newPreferences, defaultPreferences);
+ // delete temp file
+ toTempFile.delete();
+ }
+
+ public static void copySharedPreferences(SharedPreferences fromPreferences, SharedPreferences toPreferences) {
+ SharedPreferences.Editor editor = toPreferences.edit();
+ editor.clear();
+ copySharedPreferences(fromPreferences, editor);
+ editor.commit();
+ }
+
+ public static void copySharedPreferences(SharedPreferences fromPreferences, SharedPreferences.Editor toEditor) {
+ for (Map.Entry entry : fromPreferences.getAll().entrySet()) {
+ Object value = entry.getValue();
+ String key = entry.getKey();
+ //Log.w(TAG, "copySharedPreferences: Key = " + key + ", Value = " + value.toString());
+ if (value instanceof String) {
+ toEditor.putString(key, ((String) value));
+ } else if (value instanceof Set) {
+ toEditor.putStringSet(key, (Set) value); // EditorImpl.putStringSet already creates a copy of the set
+ } else if (value instanceof Integer) {
+ toEditor.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ toEditor.putLong(key, (Long) value);
+ } else if (value instanceof Float) {
+ toEditor.putFloat(key, (Float) value);
+ } else if (value instanceof Boolean) {
+ toEditor.putBoolean(key, (Boolean) value);
+ }
+ }
+ }
+
+ // Replace the secrets with the new versions. These values might change when the backup is
+ // restored on another device as where it is made.
+ private static void importSecrets(Context context) {
+ DatabaseSecret dbs = getDatabaseSecretFromBackup(context);
+ AttachmentSecret ats = getAttachmentSecretFromBackup(context);
+ byte[] lgs = getLogSecretFromBackup(context);
+ String bks = getBackupKeyFromBackup(context);
+
+ if (dbs != null) {
+ overwriteDatabaseSecret(context, dbs);
+ }
+ if (ats != null) {
+ overwriteAttachmentSecret(context, ats);
+ }
+ if (lgs != null) {
+ overwriteLogSecret(context, lgs);
+ }
+ if (bks != null) {
+ BackupPassphrase.set(context, bks);
+ }
+ }
+
+ private static String getExportDatabaseSecretFullName(Context context) {
+ return getExportSecretsDirectory(context) + databaseSecretFile;
+ }
+
+ private static String getExportAttachmentSecretFullName(Context context) {
+ return getExportSecretsDirectory(context) + attachmentSecretFile;
+ }
+
+ private static String getExportLogSecretFullName(Context context) {
+ return getExportSecretsDirectory(context) + logSecretFile;
+ }
+
+ private static String getExportBackupKeyFullName(Context context) {
+ return getExportSecretsDirectory(context) + backupKeyFile;
+ }
+
+ private static String getExportBaseDirectory(Context context) {
+ String basedir = Environment.getExternalStorageDirectory().getAbsolutePath();
+ try {
+ basedir = StorageUtil.getRawBackupDirectory().getAbsolutePath();
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, "getExportBaseDirectory failed: " + e.toString());
+ }
+ return basedir;
+ }
+
+ private static String getExportSecretsDirectory(Context context) {
+ return getExportBaseDirectory(context) + File.separator + secretsExportDirectory + File.separator;
+ }
+
+ private static String getExportDirectoryPath(Context context) {
+ return getExportBaseDirectory(context) + File.separator + exportDirectory;
+ }
+
+ private static void verifyExternalStorageForExport(Context context) throws NoExternalStorageException {
+ if (!Environment.getExternalStorageDirectory().canWrite())
+ throw new NoExternalStorageException();
+
+ String exportDirectoryPath = getExportDirectoryPath(context);
+ File exportDirectory = new File(exportDirectoryPath);
+
+ if (!exportDirectory.exists())
+ exportDirectory.mkdir();
+ }
+
+ private static void verifyExternalStorageForImport(Context context) throws NoExternalStorageException {
+ if (!Environment.getExternalStorageDirectory().canRead() ||
+ !(new File(getExportDirectoryPath(context)).exists()))
+ throw new NoExternalStorageException();
+ }
+
+ private static void migrateFile(File from, File to) {
+ try {
+ if (from.exists()) {
+ FileChannel source = new FileInputStream(from).getChannel();
+ FileChannel destination = new FileOutputStream(to).getChannel();
+
+ destination.transferFrom(source, 0, source.size());
+ source.close();
+ destination.close();
+ }
+ } catch (IOException ioe) {
+ Log.w(TAG, ioe);
+ }
+ }
+
+ private static void exportDirectory(Context context, String directoryName) throws IOException {
+ if (!directoryName.equals("/lib") && !directoryName.equals("/code_cache") && !directoryName.equals("/cache")) {
+ File directory = new File(context.getFilesDir().getParent() + File.separatorChar + directoryName);
+ File exportDirectory = new File(getExportDirectoryPath(context) + File.separatorChar + directoryName);
+
+ if (directory.exists()) {
+ exportDirectory.mkdirs();
+
+ File[] contents = directory.listFiles();
+
+ if (contents == null)
+ throw new IOException("directory.listFiles() is null for " + context.getFilesDir().getParent() + File.separatorChar + directoryName + "!");
+
+ for (int i=0;i= Build.VERSION_CODES.M) {
+ KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
+ TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
+ } else {
+ TextSecurePreferences.setDatabaseUnencryptedSecret(context, databaseSecret.asString());
+ }
+ }
+
+ private static void overwriteAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
+ TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
+ } else {
+ TextSecurePreferences.setAttachmentUnencryptedSecret(context, attachmentSecret.serialize());
+ }
+ }
+
+ private static void overwriteLogSecret(@NonNull Context context, @NonNull byte[] lgs) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(lgs);
+ TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize());
+ } else {
+ TextSecurePreferences.setLogUnencryptedSecret(context, Base64.encodeWithPadding(lgs));
+ }
+ }
+
+ // LogSecretProvider class functions. Copied here because they are all private
+ static byte[] getOrCreateLogSecret(@NonNull Context context) {
+ String unencryptedSecret = TextSecurePreferences.getLogUnencryptedSecret(context);
+ String encryptedSecret = TextSecurePreferences.getLogEncryptedSecret(context);
+
+ if (unencryptedSecret != null) return parseUnencryptedSecret(unencryptedSecret);
+ else if (encryptedSecret != null) return parseEncryptedSecret(encryptedSecret);
+ else return createAndStoreSecret(context);
+ }
+
+ private static byte[] parseUnencryptedSecret(String secret) {
+ try {
+ return Base64.decode(secret);
+ } catch (IOException e) {
+ throw new AssertionError("Failed to decode the unecrypted secret.");
+ }
+ }
+
+ private static byte[] parseEncryptedSecret(String secret) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(secret);
+ return KeyStoreHelper.unseal(encryptedSecret);
+ } else {
+ throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
+ }
+ }
+
+ private static byte[] createAndStoreSecret(@NonNull Context context) {
+ SecureRandom random = new SecureRandom();
+ byte[] secret = new byte[32];
+ random.nextBytes(secret);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret);
+ TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize());
+ } else {
+ TextSecurePreferences.setLogUnencryptedSecret(context, Base64.encodeWithPadding(secret));
+ }
+ return secret;
+ }
+
+ private static String getEncryptedZipfileName() {
+ try {
+ String backupPath = StorageUtil.getRawBackupDirectory().getAbsolutePath();
+ return backupPath + File.separator + "SignalExport.zip";
+ } catch (NoExternalStorageException e) {
+ Log.w(TAG, "getEncryptedZipfileName failed: " + e.toString());
+ return Environment.getExternalStorageDirectory().getAbsolutePath();
+ }
+ }
+
+ // Delete the exported contents of the data dir and the unencrypted keys.
+ private static void deleteRawBackupFiles(Context context) {
+ FileUtilsJW.secureDeleteRecursive(new File(getExportSecretsDirectory(context)));
+ FileUtilsJW.deleteRecursive(new File(getExportDirectoryPath(context)));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt
index 12448a85887..b6ce607f0d3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt
@@ -97,6 +97,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
GROUP BY ${AttachmentTable.DATA_FILE}
"""
+// JW: don't remove link preview media from the list, we want to be able to selectively delete it.
+/*
private val GALLERY_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
@@ -116,7 +118,24 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${MessageTable.LINK_PREVIEWS} IS NULL
"""
)
+*/
+ private val GALLERY_MEDIA_QUERY = String.format(
+ BASE_MEDIA_QUERY,
+ """
+ ${AttachmentTable.DATA_FILE} IS NOT NULL AND
+ ${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
+ (${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%')
+ """
+ )
+ private val GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS = String.format(
+ BASE_MEDIA_QUERY,
+ """
+ (${AttachmentTable.DATA_FILE} IS NOT NULL OR (${AttachmentTable.CONTENT_TYPE} LIKE 'video/%' AND ${AttachmentTable.REMOTE_INCREMENTAL_DIGEST} IS NOT NULL)) AND
+ ${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
+ (${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%')
+ """
+ )
private val AUDIO_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
@@ -125,6 +144,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
"""
)
+// JW
+/*
private val ALL_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
@@ -133,6 +154,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${MessageTable.LINK_PREVIEWS} IS NULL
"""
)
+*/
+ private val ALL_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, "${AttachmentTable.DATA_FILE} IS NOT NULL AND ${AttachmentTable.CONTENT_TYPE} NOT LIKE 'text/x-signal-plain'")
private val DOCUMENT_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt
index cf32a949fc5..80975e20002 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt
@@ -5484,4 +5484,30 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
}
}
+
+ //---------------------------------------------------------------------------
+ // JW: Deletes only the attachment for the message, not the message itself.
+ fun deleteAttachmentsOnly(messageId: Long): Boolean {
+ val threadId = getThreadIdForMessage(messageId)
+ val attachmentTable = SignalDatabase.attachments
+ attachmentTable.deleteAttachmentsForMessage(messageId)
+ notifyConversationListeners(threadId)
+ OptimizeMessageSearchIndexJob.enqueue()
+ return true
+ }
+
+ // JW: added functions required for PlaintextBackup
+ fun getMessageCount(): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from(TABLE_NAME)
+ .run()
+ .readToSingleInt()
+ }
+
+ fun getMessages(skip: Int, limit: Int): Cursor {
+ val db = databaseHelper.signalReadableDatabase
+ return db.query(TABLE_NAME, MMS_PROJECTION, null, null, null, null, ID, skip.toString().plus(",").plus(limit.toString()))
+ }
+ //---------------------------------------------------------------------------
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupExporter.java
new file mode 100644
index 00000000000..74195e4d462
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupExporter.java
@@ -0,0 +1,147 @@
+package org.thoughtcrime.securesms.database;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.FileUtilsJW;
+import org.thoughtcrime.securesms.util.StorageUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+
+import java.io.File;
+import java.io.IOException;
+
+public class PlaintextBackupExporter {
+ private static final String TAG = Log.tag(PlaintextBackupExporter.class);
+
+ private static final String FILENAME = "SignalPlaintextBackup.xml";
+ private static final String ZIPFILENAME = "SignalPlaintextBackup.zip";
+
+ public static void exportPlaintextToSd(Context context)
+ throws NoExternalStorageException, IOException
+ {
+ exportPlaintext(context);
+ }
+
+ public static File getPlaintextExportFile() throws NoExternalStorageException {
+ return new File(StorageUtil.getBackupPlaintextDirectory(), FILENAME);
+ }
+
+ private static File getPlaintextZipFile() throws NoExternalStorageException {
+ return new File(StorageUtil.getBackupPlaintextDirectory(), ZIPFILENAME);
+ }
+
+ public @NonNull static String getSmsAddress(MessageRecord record) {
+ Recipient recipient = record.getFromRecipient();
+
+ if (recipient.getE164().isPresent()) {
+ return recipient.getE164().get();
+ } else if (recipient.getEmail().isPresent()) {
+ return recipient.getEmail().get();
+ } else {
+ return "null";
+ }
+ }
+
+ private static void exportPlaintext(Context context)
+ throws NoExternalStorageException, IOException
+ {
+ MessageTable messagetable = SignalDatabase.messages();
+ int count = messagetable.getMessageCount();
+ XmlBackup.Writer writer = new XmlBackup.Writer(getPlaintextExportFile().getAbsolutePath(), count);
+
+ MessageRecord record;
+
+ MessageTable.MmsReader messagereader = null;
+ int skip = 0;
+ int ROW_LIMIT = 500;
+
+ do {
+ if (messagereader != null)
+ messagereader.close();
+
+ messagereader = messagetable.mmsReaderFor(messagetable.getMessages(skip, ROW_LIMIT));
+
+ try {
+ while ((record = messagereader.getNext()) != null) {
+ XmlBackup.XmlBackupItem item =
+ new XmlBackup.XmlBackupItem(0,
+ getSmsAddress(record),
+ record.getFromRecipient().getDisplayName(context),
+ record.getDateReceived(),
+ translateToSystemBaseType(record.getType()),
+ null,
+ record.getDisplayBody(context).toString(),
+ null,
+ 1,
+ record.getDeliveryStatus(),
+ getTransportType(record),
+ record.getToRecipient().getId().toLong());
+
+ writer.writeItem(item);
+ }
+ }
+ catch (Exception e) {
+ Log.w(TAG, "messagereader.getNext() failed: " + e.getMessage());
+ }
+
+ skip += ROW_LIMIT;
+ } while (messagereader.getCount() > 0);
+
+ writer.close();
+
+ if (TextSecurePreferences.isPlainBackupInZipfile(context)) {
+ File test = new File(getPlaintextZipFile().getAbsolutePath());
+ if (test.exists()) {
+ test.delete();
+ }
+ FileUtilsJW.createEncryptedPlaintextZipfile(context, getPlaintextZipFile().getAbsolutePath(), getPlaintextExportFile().getAbsolutePath());
+ getPlaintextExportFile().delete(); // Insecure, leaves possibly recoverable plaintext on device
+ // FileUtilsJW.secureDelete(getPlaintextExportFile()); // much too slow
+ }
+ }
+
+ private static String getTransportType(MessageRecord messageRecord) {
+ String transportText = "-";
+ if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
+ transportText = "-";
+ } else if (messageRecord.isPending()) {
+ transportText = "Pending";
+ } else if (messageRecord.isPush()) {
+ transportText = "Data";
+ } else if (messageRecord.isMms()) {
+ transportText = "MMS";
+ } else {
+ transportText = "SMS";
+ }
+ return transportText;
+ }
+
+ public static int translateToSystemBaseType(long type) {
+ if (isInboxType(type)) return 1;
+ else if (isOutgoingMessageType(type)) return 2;
+ else if (isFailedMessageType(type)) return 5;
+
+ return 1;
+ }
+
+ public static boolean isInboxType(long type) {
+ return (type & MessageTypes.BASE_TYPE_MASK) == MessageTypes.BASE_INBOX_TYPE;
+ }
+
+ public static boolean isOutgoingMessageType(long type) {
+ for (long outgoingType : MessageTypes.OUTGOING_MESSAGE_TYPES) {
+ if ((type & MessageTypes.BASE_TYPE_MASK) == outgoingType)
+ return true;
+ }
+
+ return false;
+ }
+
+ public static boolean isFailedMessageType(long type) {
+ return (type & MessageTypes.BASE_TYPE_MASK) == MessageTypes.BASE_SENT_FAILED_TYPE;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java
new file mode 100644
index 00000000000..c4d7ae14472
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/PlaintextBackupImporter.java
@@ -0,0 +1,153 @@
+package org.thoughtcrime.securesms.database;
+
+import android.content.Context;
+import android.os.Environment;
+
+import net.zetetic.database.sqlcipher.SQLiteStatement;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.FileUtilsJW;
+import org.thoughtcrime.securesms.util.StorageUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+public class PlaintextBackupImporter {
+
+ private static final String TAG = Log.tag(PlaintextBackupImporter.class);
+
+ public static SQLiteStatement createMessageInsertStatement(SQLiteDatabase database) {
+ return database.compileStatement("INSERT INTO " + MessageTable.TABLE_NAME + " (" +
+ MessageTable.FROM_RECIPIENT_ID + ", " +
+ MessageTable.DATE_SENT + ", " +
+ MessageTable.DATE_RECEIVED + ", " +
+ MessageTable.READ + ", " +
+ MessageTable.MMS_STATUS + ", " +
+ MessageTable.TYPE + ", " +
+ MessageTable.BODY + ", " +
+ MessageTable.THREAD_ID + ", " +
+ MessageTable.TO_RECIPIENT_ID + ") " +
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ }
+
+ public static void importPlaintextFromSd(Context context) throws NoExternalStorageException, IOException
+ {
+ Log.i(TAG, "importPlaintext()");
+ // Unzip zipfile first if required
+ if (TextSecurePreferences.isPlainBackupInZipfile(context)) {
+ File zipFile = getPlaintextExportZipFile();
+ FileUtilsJW.extractEncryptedZipfile(context, zipFile.getAbsolutePath(), StorageUtil.getBackupPlaintextDirectory().getAbsolutePath());
+ }
+ MessageTable table = SignalDatabase.messages();
+ SQLiteDatabase transaction = table.beginTransaction();
+
+ try {
+ ThreadTable threadTable = SignalDatabase.threads();
+ XmlBackup backup = new XmlBackup(getPlaintextExportFile().getAbsolutePath());
+ Set modifiedThreads = new HashSet<>();
+ XmlBackup.XmlBackupItem item;
+
+ // TODO: we might have to split this up in chunks of about 5000 messages to prevent these errors:
+ // java.util.concurrent.TimeoutException: net.sqlcipher.database.SQLiteCompiledSql.finalize() timed out after 10 seconds
+ while ((item = backup.getNext()) != null) {
+ Recipient recipient = Recipient.external(context, item.getAddress());
+ long threadId = threadTable.getOrCreateThreadIdFor(recipient);
+ SQLiteStatement statement = createMessageInsertStatement(transaction);
+
+ if (item.getAddress() == null || item.getAddress().equals("null"))
+ continue;
+
+ if (!isAppropriateTypeForImport(item.getType()))
+ continue;
+
+ addStringToStatement(statement, 1, recipient.getId().serialize());
+ addLongToStatement(statement, 2, item.getDate());
+ addLongToStatement(statement, 3, item.getDate());
+ addLongToStatement(statement, 4, item.getRead());
+ addLongToStatement(statement, 5, item.getStatus());
+ addTranslatedTypeToStatement(statement, 6, item.getType());
+ addStringToStatement(statement, 7, item.getBody());
+ addLongToStatement(statement, 8, threadId);
+ addLongToStatement(statement, 9, item.getRecipient());
+ modifiedThreads.add(threadId);
+ //statement.execute();
+ long rowId = statement.executeInsert();
+ }
+
+ for (long threadId : modifiedThreads) {
+ threadTable.update(threadId, true, true);
+ }
+
+ table.setTransactionSuccessful();
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, e);
+ throw new IOException("XML Parsing error!");
+ } finally {
+ table.endTransaction(transaction);
+ }
+ // Delete the plaintext file if zipfile is present
+ if (TextSecurePreferences.isPlainBackupInZipfile(context)) {
+ getPlaintextExportFile().delete(); // Insecure, leaves possibly recoverable plaintext on device
+ // FileUtilsJW.secureDelete(getPlaintextExportFile()); // much too slow
+ }
+ }
+
+ private static File getPlaintextExportFile() throws NoExternalStorageException {
+ File backup = new File(StorageUtil.getBackupPlaintextDirectory(), "SignalPlaintextBackup.xml");
+ File previousBackup = new File(StorageUtil.getLegacyBackupDirectory(), "SignalPlaintextBackup.xml");
+ File oldBackup = new File(Environment.getExternalStorageDirectory(), "TextSecurePlaintextBackup.xml");
+
+ if (backup.exists()) return backup;
+ else if (previousBackup.exists()) return previousBackup;
+ else if (oldBackup.exists()) return oldBackup;
+ else return backup;
+ }
+
+ private static File getPlaintextExportZipFile() throws NoExternalStorageException {
+ return new File(StorageUtil.getBackupPlaintextDirectory(), "SignalPlaintextBackup.zip");
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static void addTranslatedTypeToStatement(SQLiteStatement statement, int index, int type) {
+ statement.bindLong(index, translateFromSystemBaseType(type));
+ }
+
+ private static void addStringToStatement(SQLiteStatement statement, int index, String value) {
+ if (value == null || value.equals("null")) statement.bindNull(index);
+ else statement.bindString(index, value);
+ }
+
+ private static void addNullToStatement(SQLiteStatement statement, int index) {
+ statement.bindNull(index);
+ }
+
+ private static void addLongToStatement(SQLiteStatement statement, int index, long value) {
+ statement.bindLong(index, value);
+ }
+
+ private static boolean isAppropriateTypeForImport(long theirType) {
+ long ourType = translateFromSystemBaseType(theirType);
+
+ return ourType == MessageTypes.BASE_INBOX_TYPE ||
+ ourType == MessageTypes.BASE_SENT_TYPE ||
+ ourType == MessageTypes.BASE_SENT_FAILED_TYPE;
+ }
+
+ public static long translateFromSystemBaseType(long theirType) {
+ switch ((int)theirType) {
+ case 1: return MessageTypes.BASE_INBOX_TYPE;
+ case 2: return MessageTypes.BASE_SENT_TYPE;
+ case 3: return MessageTypes.BASE_DRAFT_TYPE;
+ case 4: return MessageTypes.BASE_OUTBOX_TYPE;
+ case 5: return MessageTypes.BASE_SENT_FAILED_TYPE;
+ case 6: return MessageTypes.BASE_OUTBOX_TYPE;
+ }
+
+ return MessageTypes.BASE_INBOX_TYPE;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackup.java b/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackup.java
new file mode 100644
index 00000000000..ccce80a9770
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackup.java
@@ -0,0 +1,267 @@
+package org.thoughtcrime.securesms.database;
+
+import android.annotation.SuppressLint;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Environment;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.attachments.UriAttachment;
+import org.thoughtcrime.securesms.util.MediaUtil;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.LinkedList;
+import java.util.List;
+
+public class WhatsappBackup {
+
+ public static final String TAG = WhatsappBackup.class.getSimpleName();
+
+ private static final String PROTOCOL = "protocol";
+ private static final String ADDRESS = "address";
+ private static final String CONTACT_NAME = "contact_name";
+ private static final String DATE = "date";
+ private static final String READABLE_DATE = "readable_date";
+ private static final String TYPE = "type";
+ private static final String SUBJECT = "subject";
+ private static final String BODY = "body";
+ private static final String SERVICE_CENTER = "service_center";
+ private static final String READ = "read";
+ private static final String STATUS = "status";
+ private static final String TOA = "toa";
+ private static final String SC_TOA = "sc_toa";
+ private static final String LOCKED = "locked";
+ private static final String TRANSPORT = "transport";
+ private static final String GROUP_NAME = "group_name";
+
+ private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
+
+ private final android.database.sqlite.SQLiteDatabase whatsappDb;
+ private long dbOffset = 0l;
+
+ public WhatsappBackup(android.database.sqlite.SQLiteDatabase whatsappDb) {
+ this.whatsappDb = whatsappDb;
+ }
+
+ @SuppressLint("Range")
+ public static List getMediaAttachments(SQLiteDatabase whatsappDb, WhatsappBackupItem item) {
+ List attachments = new LinkedList<>();
+ try {
+ Cursor c = whatsappDb.rawQuery("SELECT * FROM message_media WHERE message_row_id=" + item.getWaMessageId() +" LIMIT 1", null);
+ if (c != null) {
+ if (c.moveToFirst()) {
+ do {
+ File storagePath = Environment.getExternalStorageDirectory();
+ String filePath = storagePath.getAbsolutePath() + File.separator + "Android/media/com.whatsapp/WhatsApp" + File.separator + c.getString(c.getColumnIndex("file_path"));
+ int size = c.getInt(c.getColumnIndex("file_size"));
+ String type = c.getString(c.getColumnIndex("mime_type"));
+ File file = new File(filePath);
+ if (!file.exists()) return attachments;
+ Uri uri = Uri.fromFile(file);
+ String name = filePath;
+ if (type.equals("image/jpeg")) {
+ Attachment attachment = new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentTable.TRANSFER_PROGRESS_DONE,
+ size, name, false, false, false, false, item.getMediaCaption(), null, null, null, null);
+ attachments.add(attachment);
+ } else if (type.equals("video/mp4")) {
+ Attachment attachment = new UriAttachment(uri, MediaUtil.VIDEO_MP4, AttachmentTable.TRANSFER_PROGRESS_DONE,
+ size, name, false, false, false, false, item.getMediaCaption(), null, null, null, null);
+ attachments.add(attachment);
+ } else if (type.equals("audio/ogg; codecs=opus")) {
+ Attachment attachment = new UriAttachment(uri, MediaUtil.AUDIO_UNSPECIFIED, AttachmentTable.TRANSFER_PROGRESS_DONE,
+ size, name, true, false, false, false, null, null, null, null, null);
+ attachments.add(attachment);
+ } else {
+ return attachments; // Ignore everything that is not an image or a video for the moment
+ }
+ return attachments;
+ }
+ while (c.moveToNext());
+ }
+ c.close();
+ }
+ }catch(Exception e2){
+ Log.w(TAG, e2.getMessage());
+ }
+ return attachments;
+ }
+
+ @SuppressLint("Range")
+ public WhatsappBackup.WhatsappBackupItem getNext() {
+ WhatsappBackup.WhatsappBackupItem item = null;
+ try {
+ Cursor c = whatsappDb.rawQuery("SELECT * FROM messages LIMIT "+ dbOffset + ", 1", null);
+ if (c != null) {
+ if (c.moveToFirst()) {
+ do {
+ item = new WhatsappBackup.WhatsappBackupItem();
+ item.subject = null;
+ item.body = c.getString(c.getColumnIndex("data"));
+ item.protocol = 0;
+ String rawAddress = c.getString(c.getColumnIndex("key_remote_jid"));
+ if (rawAddress != null && rawAddress.trim().length() > 0) {
+ item.address = "+" + rawAddress.split("@")[0]; // Only keep the phone number
+ }
+ if (item.address.contains("-")) { // Check if it's a group message
+ item.groupName = getGroupName(c.getString(c.getColumnIndex("key_remote_jid")));
+ rawAddress = c.getString(c.getColumnIndex("remote_resource"));
+ if (rawAddress != null && rawAddress.trim().length() > 0) {
+ item.address = "+" + c.getString(c.getColumnIndex("remote_resource")).split("@")[0];
+ } else {
+ item.address = null;
+ }
+ }
+ item.contactName = null;
+ item.date = c.getLong(c.getColumnIndex("timestamp"));
+ item.readableDate = dateFormatter.format(item.date);
+ int fromMe = c.getInt(c.getColumnIndex("key_from_me"));
+ item.type = (int)(fromMe == 1 ? 2 : 1);
+ item.serviceCenter = null;
+ item.read = 1;
+ item.status = MessageTable.Status.STATUS_COMPLETE;
+ item.transport = "Data";
+ item.mediaWaType = c.getInt(c.getColumnIndex("media_wa_type"));
+ item.waMessageId = c.getLong(c.getColumnIndex("_id"));
+ item.mediaCaption = c.getString(c.getColumnIndex("media_caption"));
+ }
+ while (c.moveToNext());
+ }
+ c.close();
+ }
+ }catch(Exception e2){
+ Log.w(TAG, e2.getMessage());
+ }
+
+ dbOffset++;
+ //if (dbOffset == 3000) return null; // Limit number of imported messages for quick testing
+ return item;
+ }
+
+ @SuppressLint("Range")
+ private String getGroupName(String keyRemoteJid) {
+ try {
+
+ Cursor c = whatsappDb.rawQuery("SELECT subject FROM chat JOIN jid ON chat.jid_row_id=jid._id WHERE jid.raw_string='" + keyRemoteJid + "' LIMIT 1", null);
+ if (c != null) {
+ if (c.moveToFirst()) {
+ do {
+ String groupName = c.getString(c.getColumnIndex("subject"));
+ return groupName;
+ }
+ while (c.moveToNext());
+ }
+ c.close();
+ }
+ }catch(Exception e2) {
+ Log.w(TAG, e2.getMessage());
+ }
+ return null;
+ }
+
+ public static class WhatsappBackupItem {
+ private int protocol;
+ private String address;
+ private String contactName;
+ private long date;
+ private String readableDate;
+ private int type;
+ private String subject;
+ private String body;
+ private String serviceCenter;
+ private int read;
+ private int status;
+ private String transport;
+ private String groupName;
+ private int mediaWaType; //
+ private long waMessageId;
+ private String mediaCaption;
+
+ public WhatsappBackupItem() {}
+
+ public WhatsappBackupItem(int protocol, String address, String contactName, long date, int type,
+ String subject, String body, String serviceCenter, int read, int status,
+ String transport)
+ {
+ this.protocol = protocol;
+ this.address = address;
+ this.contactName = contactName;
+ this.date = date;
+ this.readableDate = dateFormatter.format(date);
+ this.type = type;
+ this.subject = subject;
+ this.body = body;
+ this.serviceCenter = serviceCenter;
+ this.read = read;
+ this.status = status;
+ this.transport = transport;
+ this.groupName = null;
+ this.mediaWaType = 0;
+ this.waMessageId = 0;
+ }
+
+ public int getProtocol() {
+ return protocol;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getContactName() {
+ return contactName;
+ }
+
+ public long getDate() {
+ return date;
+ }
+
+ public String getReadableDate() {
+ return readableDate;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public String getServiceCenter() {
+ return serviceCenter;
+ }
+
+ public int getRead() {
+ return read;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public int getMediaWaType() {
+ return mediaWaType;
+ }
+
+ public long getWaMessageId() {
+ return waMessageId;
+ }
+
+ public String getTransport() { return transport; }
+
+ public String getMediaCaption() {
+ return mediaCaption;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackupImporter.java
new file mode 100644
index 00000000000..72fe8bbdd72
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/WhatsappBackupImporter.java
@@ -0,0 +1,273 @@
+package org.thoughtcrime.securesms.database;
+
+import android.app.ProgressDialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+//import com.google.android.mms.pdu_alt.PduHeaders;
+
+import net.zetetic.database.sqlcipher.SQLiteStatement;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.attachments.AttachmentId;
+import org.thoughtcrime.securesms.database.model.GroupRecord;
+import org.thoughtcrime.securesms.database.whatsapp.WaDbOpenHelper;
+import org.thoughtcrime.securesms.mms.MmsException;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.thoughtcrime.securesms.database.MessageTable.DATE_RECEIVED;
+import static org.thoughtcrime.securesms.database.MessageTable.DATE_SENT;
+import static org.thoughtcrime.securesms.database.MessageTable.TYPE;
+import static org.thoughtcrime.securesms.database.MessageTable.MMS_MESSAGE_TYPE;
+import static org.thoughtcrime.securesms.database.MessageTable.MMS_STATUS;
+import static org.thoughtcrime.securesms.database.MessageTable.TABLE_NAME;
+import static org.thoughtcrime.securesms.database.MessageTable.VIEW_ONCE;
+import static org.thoughtcrime.securesms.database.MessageTable.DATE_SERVER;
+import static org.thoughtcrime.securesms.database.MessageTable.EXPIRES_IN;
+import static org.thoughtcrime.securesms.database.MessageTable.READ;
+import static org.thoughtcrime.securesms.database.MessageTable.FROM_RECIPIENT_ID;
+import static org.thoughtcrime.securesms.database.MessageTable.SMS_SUBSCRIPTION_ID;
+import static org.thoughtcrime.securesms.database.MessageTable.THREAD_ID;
+import static org.thoughtcrime.securesms.database.MessageTable.BASE_INBOX_TYPE;
+import static org.thoughtcrime.securesms.database.MessageTable.BASE_SENT_TYPE;
+import static org.thoughtcrime.securesms.database.MessageTable.UNIDENTIFIED;
+
+public class WhatsappBackupImporter {
+ // JW: from com.google.android.mms.pdu_alt.PduHeaders
+ public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
+
+ private static final String TAG = org.thoughtcrime.securesms.database.PlaintextBackupImporter.class.getSimpleName();
+
+ private static android.database.sqlite.SQLiteDatabase openWhatsappDb(Context context) throws NoExternalStorageException {
+ try {
+ android.database.sqlite.SQLiteOpenHelper db = new WaDbOpenHelper(context);
+ android.database.sqlite.SQLiteDatabase newdb = db.getReadableDatabase();
+ return newdb;
+ } catch(Exception e2){
+ throw new NoExternalStorageException();
+ }
+ }
+
+ public static void importWhatsappFromSd(Context context, ProgressDialog progressDialog, boolean importGroups, boolean avoidDuplicates, boolean importMedia)
+ throws NoExternalStorageException, IOException
+ {
+ Log.w(TAG, "importWhatsapp(): importGroup: " + importGroups + ", avoidDuplicates: " + avoidDuplicates);
+ android.database.sqlite.SQLiteDatabase whatsappDb = openWhatsappDb(context);
+ MessageTable messageDb = SignalDatabase.messages();
+ //MmsTable mmsDb = SignalDatabase.mms();
+ AttachmentTable attachmentDb = SignalDatabase.attachments();
+ SQLiteDatabase smsDbTransaction = messageDb.beginTransaction();
+ int numMessages = getNumMessages(whatsappDb, importMedia);
+ progressDialog.setMax(numMessages);
+ try {
+ ThreadTable threads = SignalDatabase.threads();
+ GroupTable groups = SignalDatabase.groups();
+ WhatsappBackup backup = new WhatsappBackup(whatsappDb);
+ Set modifiedThreads = new HashSet<>();
+ WhatsappBackup.WhatsappBackupItem item;
+
+ int msgCount = 0;
+ while ((item = backup.getNext()) != null) {
+ msgCount++;
+ progressDialog.setProgress(msgCount);
+ Recipient recipient = getRecipient(context, item);
+ if (isGroupMessage(item) && !importGroups) continue;
+ long threadId = getThreadId(item, groups, threads, recipient);
+
+ if (threadId == -1) continue;
+
+ if (isMms(item)) {
+ if (!importMedia) continue;
+ if (avoidDuplicates && wasMsgAlreadyImported(smsDbTransaction, MessageTable.TABLE_NAME, MessageTable.DATE_SENT, threadId, recipient, item)) continue;
+ List attachments = WhatsappBackup.getMediaAttachments(whatsappDb, item);
+ if (attachments != null && attachments.size() > 0) insertMms(messageDb, attachmentDb, item, recipient, threadId, attachments);
+ } else {
+ if (item.getBody() == null) continue; //Ignore empty msgs for e.g. change of security numbers
+ if (avoidDuplicates && wasMsgAlreadyImported(smsDbTransaction, MessageTable.TABLE_NAME, MessageTable.DATE_SENT, threadId, recipient, item)) continue;
+ insertSms(messageDb, smsDbTransaction, item, recipient, threadId);
+ }
+ modifiedThreads.add(threadId);
+ }
+
+ for (long threadId : modifiedThreads) {
+ threads.update(threadId, true, true);
+ }
+
+ whatsappDb.setTransactionSuccessful();
+ Log.w(TAG, "Exited loop");
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ throw new IOException("Whatsapp Import error!");
+ } finally {
+ whatsappDb.close();
+ messageDb.endTransaction(smsDbTransaction);
+ }
+
+ }
+
+ private static boolean wasMsgAlreadyImported(SQLiteDatabase db, String tableName, String dateField, long threadId, Recipient recipient, WhatsappBackup.WhatsappBackupItem item) {
+ String[] cols = new String[] {"COUNT(*)"};
+ String query = THREAD_ID + " = ? AND " + dateField + " = ? AND " + FROM_RECIPIENT_ID + " = ?";
+ String[] args = new String[]{String.valueOf(threadId), String.valueOf(item.getDate()), String.valueOf(recipient.getId().serialize())};
+
+ try (Cursor cursor = db.query(tableName, cols, query, args, null, null, null)) {
+ if (cursor != null) {
+ if (cursor.moveToFirst() && cursor.getInt(0) > 0) {
+ cursor.close();
+ return true;
+ }
+ cursor.close();
+ }
+ }
+ return false;
+ }
+
+ private static int getNumMessages(android.database.sqlite.SQLiteDatabase whatsappDb, boolean importMedia) {
+ String whereClause = "";
+ if (!importMedia) whereClause = " WHERE data!=''";
+ try {
+ Cursor c = whatsappDb.rawQuery("SELECT COUNT(*) FROM messages" + whereClause, null);
+ if (c != null) {
+ if (c.moveToFirst()) {
+ int count = c.getInt(0);
+ return count;
+ }
+ c.close();
+ }
+ }catch(Exception e2){
+ Log.w(TAG, e2.getMessage());
+ }
+ return 0;
+ }
+
+ private static Recipient getRecipient(Context context, WhatsappBackup.WhatsappBackupItem item) {
+ Recipient recipient;
+ if (item.getAddress() == null) {
+ recipient = Recipient.self();
+ } else {
+ recipient = Recipient.external(context, item.getAddress());
+ }
+ return recipient;
+ }
+
+ private static long getThreadId(WhatsappBackup.WhatsappBackupItem item, GroupTable groups, ThreadTable threads, Recipient recipient) {
+ long threadId;
+ if (isGroupMessage(item)) {
+ RecipientId threadRecipientId = getGroupId(groups, item, recipient);
+ if (threadRecipientId == null) return -1;
+ try {
+ Recipient threadRecipient = Recipient.resolved(threadRecipientId);
+ threadId = threads.getOrCreateThreadIdFor(threadRecipient);
+ } catch (Exception e) {
+ Log.v(TAG, "Group not found: " + item.getGroupName());
+ return -1;
+ }
+ } else {
+ threadId = threads.getOrCreateThreadIdFor(recipient);
+ }
+ return threadId;
+ }
+
+ private static boolean isMms(WhatsappBackup.WhatsappBackupItem item) {
+ if (item.getMediaWaType() != 0) return true;
+ return false;
+ }
+
+ private static void insertMms(MessageTable mmsDb, AttachmentTable attachmentDb, WhatsappBackup.WhatsappBackupItem item, Recipient recipient, long threadId, List attachments) throws MmsException {
+ List quoteAttachments = new LinkedList<>();
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(DATE_SENT, item.getDate());
+ contentValues.put(DATE_SERVER, item.getDate());
+ contentValues.put(FROM_RECIPIENT_ID, recipient.getId().serialize());
+ if (item.getType() == 1) {
+ contentValues.put(TYPE, BASE_INBOX_TYPE);
+ } else {
+ contentValues.put(TYPE, BASE_SENT_TYPE);
+ }
+ contentValues.put(MMS_MESSAGE_TYPE, MESSAGE_TYPE_RETRIEVE_CONF);
+ contentValues.put(THREAD_ID, threadId);
+ contentValues.put(MMS_STATUS, MessageTable.MmsStatus.DOWNLOAD_INITIALIZED);
+ contentValues.put(DATE_RECEIVED, item.getDate());
+ contentValues.put(SMS_SUBSCRIPTION_ID, -1);
+ contentValues.put(EXPIRES_IN, 0);
+ contentValues.put(VIEW_ONCE, 0);
+ contentValues.put(READ, 1);
+ contentValues.put(UNIDENTIFIED, 0);
+
+ SQLiteDatabase transaction = mmsDb.beginTransaction();
+ long messageId = transaction.insert(TABLE_NAME, null, contentValues);
+
+ Map insertedAttachments = attachmentDb.insertAttachmentsForMessage(messageId, attachments, quoteAttachments);
+ mmsDb.setTransactionSuccessful();
+ mmsDb.endTransaction();
+
+ }
+
+ private static void insertSms(MessageTable smsDb, SQLiteDatabase transaction, WhatsappBackup.WhatsappBackupItem item, Recipient recipient, long threadId) {
+ SQLiteStatement statement = PlaintextBackupImporter.createMessageInsertStatement(transaction);
+
+ addStringToStatement(statement, 1, recipient.getId().serialize());
+ addLongToStatement(statement, 2, item.getDate());
+ addLongToStatement(statement, 3, item.getDate());
+ addLongToStatement(statement, 4, item.getRead());
+ addLongToStatement(statement, 5, item.getStatus());
+ addTranslatedTypeToStatement(statement, 6, item.getType());
+ addStringToStatement(statement, 7, item.getBody());
+ addLongToStatement(statement, 8, threadId);
+
+ statement.execute();
+ statement.close();
+ }
+
+ private static RecipientId getGroupId(GroupTable groups, WhatsappBackup.WhatsappBackupItem item, Recipient recipient) {
+ if (item.getGroupName() == null) return null;
+ List groupRecords = groups.getGroupsContainingMember(recipient.getId(), false);
+ for (GroupRecord group : groupRecords) {
+ if (group.getTitle().equals(item.getGroupName())) {
+ return group.getRecipientId();
+ }
+ }
+ return null;
+ }
+
+ private static boolean isGroupMessage(WhatsappBackup.WhatsappBackupItem item) {
+ if (item.getGroupName() != null) return true;
+ return false;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static void addTranslatedTypeToStatement(SQLiteStatement statement, int index, int type) {
+ statement.bindLong(index, PlaintextBackupImporter.translateFromSystemBaseType(type));
+ }
+
+ private static void addStringToStatement(SQLiteStatement statement, int index, String value) {
+ if (value == null || value.equals("null")) statement.bindNull(index);
+ else statement.bindString(index, value);
+ }
+
+ private static void addNullToStatement(SQLiteStatement statement, int index) {
+ statement.bindNull(index);
+ }
+
+ private static void addLongToStatement(SQLiteStatement statement, int index, long value) {
+ statement.bindLong(index, value);
+ }
+
+ private static boolean isAppropriateTypeForImport(long theirType) {
+ long ourType = PlaintextBackupImporter.translateFromSystemBaseType(theirType);
+
+ return ourType == BASE_INBOX_TYPE ||
+ ourType == MessageTable.BASE_SENT_TYPE ||
+ ourType == MessageTable.BASE_SENT_FAILED_TYPE;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java b/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java
index a206825a013..9a5c317ae8c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java
@@ -32,6 +32,8 @@ public class XmlBackup {
private static final String TOA = "toa";
private static final String SC_TOA = "sc_toa";
private static final String LOCKED = "locked";
+ private static final String TRANSPORT = "transport";
+ private static final String RECIPIENT = "torecipient";
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
@@ -77,11 +79,11 @@ public XmlBackupItem getNext() throws IOException, XmlPullParserException {
else if (attributeName.equals(SERVICE_CENTER)) item.serviceCenter = parser.getAttributeValue(i);
else if (attributeName.equals(READ )) item.read = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(STATUS )) item.status = Integer.parseInt(parser.getAttributeValue(i));
+ else if (attributeName.equals(TRANSPORT )) item.transport = parser.getAttributeValue(i);
+ else if (attributeName.equals(RECIPIENT )) item.torecipient = Long.parseLong(parser.getAttributeValue(i));
}
-
return item;
}
-
return null;
}
@@ -97,11 +99,13 @@ public static class XmlBackupItem {
private String serviceCenter;
private int read;
private int status;
-
+ private String transport;
+ private long torecipient;
public XmlBackupItem() {}
public XmlBackupItem(int protocol, String address, String contactName, long date, int type,
- String subject, String body, String serviceCenter, int read, int status)
+ String subject, String body, String serviceCenter, int read, int status,
+ String transport, long torecipient)
{
this.protocol = protocol;
this.address = address;
@@ -114,6 +118,8 @@ public XmlBackupItem(int protocol, String address, String contactName, long date
this.serviceCenter = serviceCenter;
this.read = read;
this.status = status;
+ this.transport = transport;
+ this.torecipient = torecipient;
}
public int getProtocol() {
@@ -159,6 +165,9 @@ public int getRead() {
public int getStatus() {
return status;
}
+
+ public String getTransport() { return transport; }
+ public long getRecipient() { return torecipient; }
}
public static class Writer {
@@ -204,6 +213,8 @@ public void writeItem(XmlBackupItem item) throws IOException {
appendAttribute(stringBuilder, READ, item.getRead());
appendAttribute(stringBuilder, STATUS, item.getStatus());
appendAttribute(stringBuilder, LOCKED, 0);
+ appendAttribute(stringBuilder, TRANSPORT, item.getTransport());
+ appendAttribute(stringBuilder, RECIPIENT, item.getRecipient());
stringBuilder.append(CLOSE_EMPTYTAG);
bufferedWriter.newLine();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbContext.java b/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbContext.java
new file mode 100644
index 00000000000..edd624c231e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbContext.java
@@ -0,0 +1,57 @@
+package org.thoughtcrime.securesms.database.whatsapp;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Environment;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.database.NoExternalStorageException;
+
+import java.io.File;
+
+/**
+ * Created by Man on 10/31/2016.
+ */
+
+public class WaDbContext extends ContextWrapper {
+
+ public WaDbContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ File sdcard = Environment.getExternalStorageDirectory();
+ String dbfile = sdcard.getAbsolutePath() + File.separator + name;
+ if (!dbfile.endsWith(".db")) {
+ dbfile += ".db" ;
+ }
+
+ File result = new File(dbfile);
+
+ if (Log.isLoggable("DEBUG_CONTEXT", Log.WARN)) {
+ Log.w("DEBUG_CONTEXT", "getDatabasePath(" + name + ") = " + result.getAbsolutePath());
+ }
+
+ return result;
+ }
+
+ /* this version is called for android devices >= api-11. thank to @damccull for fixing this. */
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
+ return openOrCreateDatabase(name,mode, factory);
+ }
+
+ /* this version is called for android devices < api-11 */
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) {
+ SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
+ // SQLiteDatabase result = super.openOrCreateDatabase(name, mode, factory);
+ if (Log.isLoggable("DEBUG_CONTEXT", Log.WARN)) {
+ Log.w("DEBUG_CONTEXT", "openOrCreateDatabase(" + name + ",,) = " + result.getPath());
+ }
+ return result;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbOpenHelper.java
new file mode 100644
index 00000000000..caf8569dec9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/whatsapp/WaDbOpenHelper.java
@@ -0,0 +1,26 @@
+package org.thoughtcrime.securesms.database.whatsapp;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+
+public class WaDbOpenHelper extends SQLiteOpenHelper {
+
+ private static String DB_NAME = "msgstore.db";
+
+ public WaDbOpenHelper(Context context)
+ {
+ super(new WaDbContext(context), DB_NAME, null, 3);
+
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt
index a6fc03550b5..fd4f46a869a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.util.TextSecurePreferences // JW
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
@@ -553,6 +554,19 @@ class GroupsV2StateProcessor private constructor(
@VisibleForTesting
internal class ProfileAndMessageHelper(private val aci: ACI, private val masterKey: GroupMasterKey, private val groupId: GroupId.V2) {
+ // JW: Check if the person is allowed to add you to a group
+ private fun mayThisPersonAddYouToAGroup(addedBy: Recipient): Boolean {
+ when (TextSecurePreferences.whoCanAddYouToGroups(AppDependencies.application)) {
+ "anyone" -> return true
+ "nonblocked" -> return !addedBy.isBlocked
+ "onlycontacts" -> return addedBy.isProfileSharing && !addedBy.isBlocked // check for blocked is not necessary but defensive programming if something changes here
+ "onlysystemcontacts" -> return addedBy.isSystemContact && !addedBy.isBlocked
+ "nobody" -> return false
+ else -> return true
+ }
+ return true
+ }
+
fun setProfileSharing(groupStateDiff: GroupStateDiff, newLocalState: DecryptedGroup) {
val previousGroupState = groupStateDiff.previousGroupState
@@ -577,11 +591,12 @@ class GroupsV2StateProcessor private constructor(
if (addedBy != null) {
Log.i(TAG, "Added as a full member of $groupId by ${addedBy.id}")
- if (addedBy.isBlocked && (previousGroupState == null || !DecryptedGroupUtil.isRequesting(previousGroupState, aci))) {
+ // JW: changed logic with more options
+ if (!mayThisPersonAddYouToAGroup(addedBy) && (previousGroupState == null || !DecryptedGroupUtil.isRequesting(previousGroupState, aci))) {
Log.i(TAG, "Added by a blocked user. Leaving group.")
AppDependencies.jobManager.add(LeaveGroupV2Job(groupId))
return
- } else if ((addedBy.isSystemContact || addedBy.isProfileSharing) && !addedBy.isHidden) {
+ } else if ((addedBy.isSystemContact || addedBy.isProfileSharing) && !addedBy.isHidden && !addedBy.isBlocked) { // JW: added isBlocked explicitly here
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact + ", profileSharing: " + addedBy.isProfileSharing)
Log.i(TAG, "Added to a group and auto-enabling profile sharing")
SignalDatabase.recipients.setProfileSharing(Recipient.externalGroupExact(groupId).id, true)
@@ -594,7 +609,7 @@ class GroupsV2StateProcessor private constructor(
} else if (selfAsPending != null) {
val addedBy = UuidUtil.fromByteStringOrNull(selfAsPending.addedByAci)?.let { Recipient.externalPush(ACI.from(it)) }
- if (addedBy?.isBlocked == true) {
+ if (!mayThisPersonAddYouToAGroup(addedBy!!)) { // JW: replaced blocked by more general permission
Log.i(TAG, "Added to group $groupId by a blocked user ${addedBy.id}. Leaving group.")
AppDependencies.jobManager.add(LeaveGroupV2Job(groupId))
return
@@ -735,7 +750,7 @@ class GroupsV2StateProcessor private constructor(
} catch (e: MmsException) {
Log.w(TAG, "Failed to insert outgoing update message!", e)
}
- } else {
+ } else if (!TextSecurePreferences.whoCanAddYouToGroups(AppDependencies.application).equals("nonblocked") || !Recipient.resolved(RecipientId.from(editor.get())).isBlocked) { // JW: don't store messages from blocked contacts
try {
val isGroupAdd = updateDescription
.groupChangeUpdate!!
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt
index 0f8f6a66e8e..073c94236de 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt
@@ -71,7 +71,7 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
/**
* Whether or not the client is currently in a 'deprecated' state, disallowing network access.
*/
- var isClientDeprecated: Boolean by booleanValue(CLIENT_DEPRECATED, false)
+ var isClientDeprecated: Boolean = false // JW
/**
* Whether or not we've locked the device after they've transferred to a new one.
diff --git a/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java b/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java
index e34884e83a5..c8afa4a0388 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java
@@ -16,6 +16,7 @@
import android.os.Bundle;
import android.view.View;
import android.view.animation.OvershootInterpolator;
+import android.widget.Button; // JW
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -41,6 +42,7 @@
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences; // JW: added
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
@@ -74,6 +76,10 @@ public final class PlacePickerActivity extends AppCompatActivity {
private LatLng currentLocation = new LatLng(0, 0);
private AddressLookup addressLookup;
private GoogleMap googleMap;
+ // JW: added buttons
+ private Button btnMapTypeNormal;
+ private Button btnMapTypeSatellite;
+ private Button btnMapTypeTerrain;
public static void startActivityForResultAtCurrentLocation(@NonNull Fragment fragment, int requestCode, @ColorInt int chatColor) {
fragment.startActivityForResult(new Intent(fragment.requireActivity(), PlacePickerActivity.class).putExtra(KEY_CHAT_COLOR, chatColor), requestCode);
@@ -94,10 +100,36 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
bottomSheet = findViewById(R.id.bottom_sheet);
View markerImage = findViewById(R.id.marker_image_view);
View fab = findViewById(R.id.place_chosen_button);
+ // JW: add maptype buttons
+ btnMapTypeNormal = findViewById(R.id.btnMapTypeNormal);
+ btnMapTypeSatellite = findViewById(R.id.btnMapTypeSatellite);
+ btnMapTypeTerrain = findViewById(R.id.btnMapTypeTerrain);
ViewCompat.setBackgroundTintList(fab, ColorStateList.valueOf(getIntent().getIntExtra(KEY_CHAT_COLOR, Color.RED)));
fab.setOnClickListener(v -> finishWithAddress());
+ // JW: button event handlers
+ btnMapTypeNormal.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(@NonNull View v) {
+ googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
+ TextSecurePreferences.setGoogleMapType(getApplicationContext(), "normal");
+ }
+ });
+
+ btnMapTypeSatellite.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(@NonNull View v) {
+ googleMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
+ TextSecurePreferences.setGoogleMapType(getApplicationContext(), "satellite");
+ }
+ });
+
+ btnMapTypeTerrain.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(@NonNull View v) {
+ googleMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
+ TextSecurePreferences.setGoogleMapType(getApplicationContext(), "terrain");
+ }
+ });
+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)
{
@@ -167,10 +199,31 @@ private void setInitialLocation(@NonNull LatLng latLng) {
private void setMap(GoogleMap googleMap) {
this.googleMap = googleMap;
+ // JW: set maptype
+ if (googleMap != null) {
+ googleMap.setMapType(getGoogleMapType());
+ } else {
+ // In case there is no Google maps installed:
+ btnMapTypeNormal.setVisibility(View.GONE);
+ btnMapTypeSatellite.setVisibility(View.GONE);
+ btnMapTypeTerrain.setVisibility(View.GONE);
+ }
moveMapToInitialIfPossible();
}
+ // JW: get the maptype
+ public int getGoogleMapType() {
+ switch (TextSecurePreferences.getGoogleMapType(getApplicationContext())) {
+ case "hybrid": return GoogleMap.MAP_TYPE_HYBRID;
+ case "satellite": return GoogleMap.MAP_TYPE_SATELLITE;
+ case "terrain": return GoogleMap.MAP_TYPE_TERRAIN;
+ case "none": return GoogleMap.MAP_TYPE_NONE;
+ default: return GoogleMap.MAP_TYPE_NORMAL;
+ }
+ }
+
+
private void moveMapToInitialIfPossible() {
if (initialLocation != null && googleMap != null) {
Log.d(TAG, "Moving map to initial location");
@@ -192,6 +245,7 @@ private void finishWithAddress() {
SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(this);
MapView mapView = findViewById(R.id.map_view);
+ SignalMapView.mapType = getGoogleMapType(); // JW
SignalMapView.snapshot(currentLocation, mapView).addListener(new ListenableFuture.Listener<>() {
@Override
public void onSuccess(Bitmap result) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
index a0c7a71a30b..76517928c24 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
@@ -115,7 +115,7 @@ private static Map buildDisplayOrder(@NonNull Context
put(Event.BACKUP_SCHEDULE_PERMISSION, shouldShowBackupSchedulePermissionMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER);
put(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, shouldShowTurnOffCircumventionMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER);
- put(Event.REMOTE_MEGAPHONE, shouldShowRemoteMegaphone(records) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(1)) : NEVER);
+ //put(Event.REMOTE_MEGAPHONE, shouldShowRemoteMegaphone(records) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(1)) : NEVER); // JW
put(Event.LINKED_DEVICE_INACTIVE, shouldShowLinkedDeviceInactiveMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)): NEVER);
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
put(Event.SET_UP_YOUR_USERNAME, shouldShowSetUpYourUsernameMegaphone(records) ? ALWAYS : NEVER);
@@ -156,8 +156,8 @@ private static boolean shouldShowLinkedDeviceInactiveMegaphone() {
return buildTurnOffCircumventionMegaphone(context);
case LINKED_DEVICE_INACTIVE:
return buildLinkedDeviceInactiveMegaphone(context);
- case REMOTE_MEGAPHONE:
- return buildRemoteMegaphone(context);
+// case REMOTE_MEGAPHONE:
+// return buildRemoteMegaphone(context); // JW
case BACKUP_SCHEDULE_PERMISSION:
return buildBackupPermissionMegaphone(context);
case SET_UP_YOUR_USERNAME:
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt
index 8ca06d701e7..39c379fc631 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.NoSuchMessageException
import org.thoughtcrime.securesms.database.PaymentTable.PublicKeyConflictException
import org.thoughtcrime.securesms.database.SignalDatabase
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions // JW
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
@@ -83,6 +84,7 @@ import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.StickerSlide
import org.thoughtcrime.securesms.notifications.v2.ConversationId
+import org.thoughtcrime.securesms.notifications.v2.ConversationId.Companion.fromMessageRecord // JW
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.Recipient.HiddenState
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -571,6 +573,17 @@ object DataMessageProcessor {
return targetMessageId
}
+ // JW: add a reaction to a message. Thanks ClauZ for the implementation
+ fun setMessageReaction(context: Context, message: DataMessage, targetMessage: MessageRecord?, reaction: String) {
+ if (targetMessage != null) {
+ val reactionEmoji = EmojiUtil.getCanonicalRepresentation(reaction)
+ val targetMessageId = MessageId(targetMessage.id)
+ val reactionRecord = ReactionRecord(reactionEmoji, Recipient.self().id, message.timestamp!!, System.currentTimeMillis())
+ reactions.addReaction(targetMessageId, reactionRecord)
+ AppDependencies.messageNotifier.updateNotification(context, fromMessageRecord(targetMessage))
+ }
+ }
+
fun handleRemoteDelete(context: Context, envelope: Envelope, message: DataMessage, senderRecipientId: RecipientId, earlyMessageCacheEntry: EarlyMessageCacheEntry?): MessageId? {
val delete = message.delete!!
@@ -579,6 +592,9 @@ object DataMessageProcessor {
val targetSentTimestamp: Long = delete.targetSentTimestamp!!
val targetMessage: MessageRecord? = SignalDatabase.messages.getMessageFor(targetSentTimestamp, senderRecipientId)
+ // JW: set a reaction to indicate the message was attempted to be remote deleted. Sender is myself, emoji is an exclamation.
+ if (TextSecurePreferences.isIgnoreRemoteDelete(context)) { setMessageReaction(context, message, targetMessage, "\u2757"); return null; }
+
return if (targetMessage != null && MessageConstraintsUtil.isValidRemoteDeleteReceive(targetMessage, senderRecipientId, envelope.serverTimestamp!!)) {
SignalDatabase.messages.markAsRemoteDelete(targetMessage)
if (targetMessage.isStory()) {
@@ -894,6 +910,7 @@ object DataMessageProcessor {
notifyTypingStoppedFromIncomingMessage(context, senderRecipient, threadRecipient.id, metadata.sourceDeviceId)
val insertResult: InsertResult?
+ val viewOnce: Boolean = if (TextSecurePreferences.isKeepViewOnceMessages(context)) false else (message.isViewOnce == true) // JW
SignalDatabase.messages.beginTransaction()
try {
@@ -914,7 +931,7 @@ object DataMessageProcessor {
serverTimeMillis = envelope.serverTimestamp!!,
receivedTimeMillis = receivedTime,
expiresIn = message.expireTimerDuration.inWholeMilliseconds,
- isViewOnce = message.isViewOnce == true,
+ isViewOnce = viewOnce, // JW
isUnidentified = metadata.sealedSender,
body = message.body?.ifEmpty { null },
groupId = groupId,
@@ -958,7 +975,12 @@ object DataMessageProcessor {
AppDependencies.messageNotifier.updateNotification(context, ConversationId.forConversation(insertResult.threadId))
TrimThreadJob.enqueueAsync(insertResult.threadId)
- if (message.isViewOnce == true) {
+ // JW: add a [1] reaction to indicate the message was sent as viewOnce.
+ if (TextSecurePreferences.isKeepViewOnceMessages(context) && (message.isViewOnce == true)) {
+ val targetMessage = SignalDatabase.messages.getMessageRecordOrNull(insertResult.messageId)
+ setMessageReaction(context, message, targetMessage, "\u0031\uFE0F\u20E3")
+ }
+ if (viewOnce) { // JW
AppDependencies.viewOnceMessageManager.scheduleIfNecessary()
}
}
@@ -1085,7 +1107,15 @@ object DataMessageProcessor {
return null
}
- val authorId = Recipient.externalPush(ServiceId.parseOrThrow(quote.authorAci!!)).id
+ // JW: copied this patch from Molly
+ //val authorId = Recipient.externalPush(ServiceId.parseOrThrow(quote.authorAci!!)).id
+ val authorAci = ServiceId.parseOrThrow(quote.authorAci!!)
+ if (authorAci.isUnknown) {
+ warn(timestamp, "Received quote with an unknown author UUID! Ignoring...")
+ return null
+ }
+ val authorId = Recipient.externalPush(authorAci).id
+ //--------------------------------
var quotedMessage = SignalDatabase.messages.getMessageFor(quote.id!!, authorId) as? MmsMessageRecord
if (quotedMessage != null && !quotedMessage.isRemoteDelete) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt
index f5ef7ca7f7d..5bbeae5dcf9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt
@@ -814,7 +814,7 @@ object SyncMessageProcessor {
val previews: List = DataMessageProcessor.getLinkPreviews(dataMessage.preview, dataMessage.body ?: "", false)
val mentions: List = DataMessageProcessor.getMentions(dataMessage.bodyRanges)
val giftBadge: GiftBadge? = if (dataMessage.giftBadge?.receiptCredentialPresentation != null) GiftBadge.Builder().redemptionToken(dataMessage.giftBadge!!.receiptCredentialPresentation!!).build() else null
- val viewOnce: Boolean = dataMessage.isViewOnce == true
+ val viewOnce: Boolean = if (TextSecurePreferences.isKeepViewOnceMessages(context)) false else (dataMessage.isViewOnce === true) // JW
val bodyRanges: BodyRangeList? = dataMessage.bodyRanges.toBodyRangeList()
val syncAttachments: List = listOfNotNull(sticker) + if (viewOnce) listOf(TombstoneAttachment(MediaUtil.VIEW_ONCE, false)) else dataMessage.attachments.toPointersWithinLimit()
@@ -1019,6 +1019,7 @@ object SyncMessageProcessor {
}
private fun handleSynchronizeViewOnceOpenMessage(context: Context, openMessage: ViewOnceOpen, envelopeTimestamp: Long, earlyMessageCacheEntry: EarlyMessageCacheEntry?) {
+ if (TextSecurePreferences.isKeepViewOnceMessages(context)) return // JW
log(envelopeTimestamp, "Handling a view-once open for message: " + openMessage.timestamp)
val author: RecipientId = Recipient.externalPush(ServiceId.parseOrThrow(openMessage.senderAci!!)).id
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java
index bd7f86529f5..a7b603c0187 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java
@@ -16,6 +16,7 @@
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.dependencies.AppDependencies; // JW: added
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -100,8 +101,14 @@ public static boolean isAutoDownloadPermitted(@NonNull Context context, @Nullabl
MessageRecord deletedMessageRecord = null;
if (attachmentCount <= 1) {
+ // JW: changed
deletedMessageRecord = SignalDatabase.messages().getMessageRecordOrNull(mmsId);
- SignalDatabase.messages().deleteMessage(mmsId);
+ if (!TextSecurePreferences.isDeleteMediaOnly(AppDependencies.getApplication())) {
+ SignalDatabase.messages().deleteMessage(mmsId);
+ } else {
+ SignalDatabase.messages().deleteAttachmentsOnly(mmsId);
+ deletedMessageRecord = null; // JW: don't propagate this delete to linked devices here
+ }
} else {
SignalDatabase.attachments().deleteAttachment(attachmentId);
if (Recipient.self().getDeleteSyncCapability().isSupported()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
index 1d56cea8efa..67085e41419 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
@@ -21,6 +21,7 @@
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.util.TextSecurePreferences; // JW: added
import java.io.File;
import java.security.SecureRandom;
@@ -50,7 +51,9 @@ public class BackupUtil {
}
public static boolean isUserSelectionRequired(@NonNull Context context) {
- return Build.VERSION.SDK_INT >= 29 && !Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ // JW: changed this because we need the manifest WRITE permission for the preservelegacy flag.
+ //return Build.VERSION.SDK_INT >= 29 && !Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ return Build.VERSION.SDK_INT >= 29 && !Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE);
}
public static boolean canUserAccessBackupDirectory(@NonNull Context context) {
@@ -189,6 +192,24 @@ private static List getAllBackupsNewestFirstApi29() {
private static List getAllBackupsNewestFirstLegacy() throws NoExternalStorageException {
File backupDirectory = StorageUtil.getOrCreateBackupDirectory();
File[] files = backupDirectory.listFiles();
+ // JW: if no backup found in internal storage, try removable storage.
+ // This code is used at first app start when restoring a backup that is located
+ // on the removable storage.
+ if (files.length == 0) {
+ Context context = AppDependencies.getApplication();
+ // This code should run only at the initial app start. In that case isBackupLocationChanged
+ // defaults to false.
+ if (!TextSecurePreferences.isBackupLocationChanged(context)) {
+ TextSecurePreferences.setBackupLocationRemovable(context, true);
+ TextSecurePreferences.setBackupLocationChanged(context, true); // Set this so we know it has been changed in the future
+ backupDirectory = StorageUtil.getBackupDirectory();
+ files = backupDirectory.listFiles();
+ if (files.length == 0) { // No backup in removable storage, reset preferences to default values
+ TextSecurePreferences.setBackupLocationRemovable(context, false);
+ TextSecurePreferences.setBackupLocationChanged(context, false);
+ }
+ }
+ }
List backups = new ArrayList<>(files.length);
for (File file : files) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FileUtilsJW.java b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtilsJW.java
new file mode 100644
index 00000000000..6e17796e95f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtilsJW.java
@@ -0,0 +1,150 @@
+package org.thoughtcrime.securesms.util;
+
+import android.content.Context;
+
+import net.lingala.zip4j.ZipFile;
+import net.lingala.zip4j.exception.ZipException;
+import net.lingala.zip4j.model.ZipParameters;
+import net.lingala.zip4j.model.enums.AesKeyStrength;
+import net.lingala.zip4j.model.enums.CompressionLevel;
+import net.lingala.zip4j.model.enums.CompressionMethod;
+import net.lingala.zip4j.model.enums.EncryptionMethod;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.backup.BackupPassphrase;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.lang.String;
+import java.security.SecureRandom;
+
+
+public class FileUtilsJW {
+
+ private static final String TAG = Log.tag(FileUtilsJW.class);
+
+ //------------------------------------------------------------------------------------------------
+ // Handle backups in encrypted zipfiles
+ public static boolean createEncryptedZipfile(Context context, String zipFileName, String exportDirectory, String exportSecretsDirectory) {
+ try {
+ String password = getBackupPassword(context);
+ ZipFile zipFile = new ZipFile(zipFileName);
+ ZipParameters parameters = new ZipParameters();
+ parameters.setCompressionMethod(CompressionMethod.STORE); // Encrypted data is uncompressable anyway
+ //parameters.setCompressionLevel(CompressionLevel.FASTEST);
+ if (password.length() > 0 ) {
+ parameters.setEncryptFiles(true);
+ parameters.setEncryptionMethod(EncryptionMethod.AES);
+ parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
+ zipFile.setPassword(password.toCharArray());
+ }
+ zipFile.addFolder(new File(exportSecretsDirectory), parameters);
+ zipFile.addFolder(new File(exportDirectory), parameters);
+ } catch (ZipException e) {
+ Log.w(TAG, "createEncryptedZipfile failed: " + e.toString());
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean createEncryptedPlaintextZipfile(Context context, String zipFileName, String inputFileName) {
+ try {
+ String password = getBackupPassword(context);
+ ZipFile zipFile = new ZipFile(zipFileName);
+ ZipParameters parameters = new ZipParameters();
+ parameters.setCompressionLevel(CompressionLevel.MAXIMUM);
+ if (password.length() > 0 ) {
+ parameters.setEncryptFiles(true);
+ parameters.setEncryptionMethod(EncryptionMethod.AES);
+ parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
+ zipFile.setPassword(password.toCharArray());
+ }
+ zipFile.addFile(inputFileName, parameters);
+ } catch (ZipException e) {
+ Log.w(TAG, "createEncryptedPlaintextZipfile failed: " + e.toString());
+ return false;
+ }
+ return true;
+ }
+
+ // Get the password of the regular backup. If there is no regular backup set, return an empty string.
+ public static String getBackupPassword(Context context) {
+ String password = "";
+ Boolean chatBackupsEnabled = SignalStore.settings().isBackupEnabled();
+ if (chatBackupsEnabled) {
+ password = BackupPassphrase.get(context);
+ if (password == null) {
+ Log.w(TAG, "createEncryptedZipfile: empty zipfile password");
+ password = "";
+ }
+ // Plaintext storage of password may contain spaces
+ password = password.replace(" ", "");
+ }
+ return password;
+ }
+
+ public static boolean extractEncryptedZipfile(Context context, String fileName, String directoryName) {
+ String password = getBackupPassword(context);
+
+ try {
+ ZipFile zipFile = new ZipFile(fileName);
+ if (zipFile.isEncrypted()) {
+ zipFile.setPassword(password.toCharArray());
+ }
+ zipFile.extractAll(directoryName);
+ } catch (Exception e) {
+ Log.w(TAG, "extractEncryptedZipfile failed: " + e.toString());
+ return false;
+ }
+ return true;
+ }
+ //------------------------------------------------------------------------------------------------
+
+ public static void deleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ for (File child : fileOrDirectory.listFiles()) {
+ deleteRecursive(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ public static void secureDeleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ for (File child : fileOrDirectory.listFiles()) {
+ secureDeleteRecursive(child);
+ }
+ }
+ try {
+ if (!fileOrDirectory.isFile()) {
+ fileOrDirectory.delete();
+ } else {
+ secureDelete(fileOrDirectory);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "secureDeleteRecursive failed: " + e.toString());
+ }
+ }
+
+ // Not perfect on wear-leveling flash memory but still better than nothing.
+ public static void secureDelete(File file) throws IOException {
+ if (file.exists()) {
+ long length = file.length();
+ SecureRandom random = new SecureRandom();
+ RandomAccessFile raf = new RandomAccessFile(file, "rws");
+ raf.seek(0);
+ raf.getFilePointer();
+ byte[] data = new byte[64];
+ long pos = 0;
+ while (pos < length) {
+ random.nextBytes(data);
+ raf.write(data);
+ pos += data.length;
+ }
+ raf.close();
+ file.delete();
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
index f2c2a708423..7d1121eaf6d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
@@ -505,12 +505,7 @@ object RemoteConfig {
/** Whether or not the user is an 'internal' one, which activates certain developer tools. */
@JvmStatic
@get:JvmName("internalUser")
- val internalUser: Boolean by remoteValue(
- key = "android.internalUser",
- hotSwappable = true
- ) { value ->
- value.asBoolean(false) || Environment.IS_NIGHTLY || Environment.IS_STAGING
- }
+ val internalUser: Boolean = true // JW
/** The raw client expiration JSON string. */
@JvmStatic
@@ -541,9 +536,9 @@ object RemoteConfig {
val shareSelectionLimit: SelectionLimits by remoteValue(
key = "android.share.limit",
- hotSwappable = true
+ hotSwappable = false // JW
) { value ->
- val limit = value.asInteger(5)
+ val limit = Integer.MAX_VALUE // JW: no forward limit
SelectionLimits(limit, limit)
}
@@ -1105,11 +1100,7 @@ object RemoteConfig {
/** Whether or not to show chat folders. */
@JvmStatic
- val showChatFolders: Boolean by remoteBoolean(
- key = "android.showChatFolders",
- defaultValue = false,
- hotSwappable = true
- )
+ val showChatFolders: Boolean = true // JW
@JvmStatic
@get:JvmName("newCallUi")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
index 728c4a88913..ff85ad26172 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java
@@ -8,34 +8,78 @@
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import com.annimon.stream.Stream; // JW: added
+
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.permissions.PermissionCompat;
+import org.thoughtcrime.securesms.keyvalue.SignalStore; // JW: added
import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.util.UriUtils; // JW: added
import java.io.File;
+import java.nio.file.Path; // JW: added
import java.util.List;
import java.util.Objects;
public class StorageUtil {
private static final String PRODUCTION_PACKAGE_ID = "org.thoughtcrime.securesms";
+ // JW: the different backup types
+ private static final String BACKUPS = "Backups";
+ private static final String FULL_BACKUPS = "FullBackups";
+ private static final String PLAINTEXT_BACKUPS = "PlaintextBackups";
- public static File getOrCreateBackupDirectory() throws NoExternalStorageException {
- File storage = Environment.getExternalStorageDirectory();
+ // JW: split backup directories per type because otherwise some files might get unintentionally deleted
+ public static File getBackupDirectory() throws NoExternalStorageException {
+ if (Build.VERSION.SDK_INT >= 30) {
+ // We don't add the separate "Backups" subdir for Android 11+ to not complicate things...
+ return getBackupTypeDirectory("");
+ } else {
+ return getBackupTypeDirectory(BACKUPS);
+ }
+ }
- if (!storage.canWrite()) {
- throw new NoExternalStorageException();
+ public static File getBackupPlaintextDirectory() throws NoExternalStorageException {
+ return getBackupTypeDirectory(PLAINTEXT_BACKUPS);
+ }
+
+ public static File getRawBackupDirectory() throws NoExternalStorageException {
+ return getBackupTypeDirectory(FULL_BACKUPS);
+ }
+
+ private static File getBackupTypeDirectory(String backupType) throws NoExternalStorageException {
+ Context context = AppDependencies.getApplication();
+ File signal = null;
+ if (Build.VERSION.SDK_INT < 30) {
+ signal = getBackupBaseDirectory();
+ } else {
+ Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
+ signal = new File(UriUtils.getFullPathFromTreeUri(context, backupDirectoryUri));
+ }
+ // For android 11+, if the last part ends with "Backups", remove that and add the backupType so
+ // we still can use the Backups, FulBackups etc. subdirectories when the chosen backup folder
+ // is a subdirectory called Backups.
+ if (Build.VERSION.SDK_INT >= 30 && !backupType.equals("")) {
+ Path selectedDir = signal.toPath();
+ if (selectedDir.endsWith(BACKUPS)) {
+ signal = selectedDir.getParent().toFile();
+ }
}
+ File backups = new File(signal, backupType);
- File backups = getBackupDirectory();
+ //noinspection ConstantConditions
+ if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) {
+ backups = new File(backups, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1));
+ }
if (!backups.exists()) {
if (!backups.mkdirs()) {
@@ -64,26 +108,72 @@ public static File getOrCreateBackupV2Directory() throws NoExternalStorageExcept
return backups;
}
- public static File getBackupDirectory() throws NoExternalStorageException {
+ public static File getBackupV2Directory() throws NoExternalStorageException {
File storage = Environment.getExternalStorageDirectory();
- File signal = new File(storage, "Signal");
- File backups = new File(signal, "Backups");
+ File backups = new File(storage, "Signal");
//noinspection ConstantConditions
if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) {
- backups = new File(backups, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1));
+ backups = new File(storage, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1));
}
return backups;
}
- public static File getBackupV2Directory() throws NoExternalStorageException {
- File storage = Environment.getExternalStorageDirectory();
- File backups = new File(storage, "Signal");
+ // JW: added. Returns storage dir on internal or removable storage
+ private static File getStorage() throws NoExternalStorageException {
+ Context context = AppDependencies.getApplication();
+ File storage = null;
+
+ // We now check if the removable storage is prefered. If it is
+ // and it is not available we fallback to internal storage.
+ if (TextSecurePreferences.isBackupLocationRemovable(context)) {
+ // For now we only support the application directory on the removable storage.
+ if (Build.VERSION.SDK_INT >= 19) {
+ File[] directories = context.getExternalFilesDirs(null);
+
+ if (directories != null) {
+ storage = Stream.of(directories)
+ .withoutNulls()
+ .filterNot(f -> f.getAbsolutePath().contains("emulated"))
+ .limit(1)
+ .findSingle()
+ .orElse(null);
+ }
+ }
+ }
+ if (storage == null) {
+ storage = Environment.getExternalStorageDirectory();
+ }
+ return storage;
+ }
- //noinspection ConstantConditions
- if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) {
- backups = new File(storage, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1));
+ // JW: added method
+ public static File getBackupBaseDirectory() throws NoExternalStorageException {
+ File storage = getStorage();
+
+ if (!storage.canWrite()) {
+ throw new NoExternalStorageException();
+ }
+
+ File signal = new File(storage, "Signal");
+
+ return signal;
+ }
+
+ public static File getOrCreateBackupDirectory() throws NoExternalStorageException {
+ File storage = getStorage(); // JW: changed
+
+ if (!storage.canWrite()) {
+ throw new NoExternalStorageException();
+ }
+
+ File backups = getBackupDirectory();
+
+ if (!backups.exists()) {
+ if (!backups.mkdirs()) {
+ throw new NoExternalStorageException("Unable to create backup directory...");
+ }
}
return backups;
@@ -113,6 +203,31 @@ public static File getBackupV2Directory() throws NoExternalStorageException {
}
}
+ public static File getBackupCacheDirectory(Context context) {
+ // JW: changed.
+ if (TextSecurePreferences.isBackupLocationRemovable(context)) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ File[] directories = context.getExternalCacheDirs();
+
+ if (directories != null) {
+ File result = getNonEmulated(directories);
+ if (result != null) return result;
+ }
+ }
+ }
+ return context.getExternalCacheDir();
+ }
+
+ // JW: re-added
+ private static @Nullable File getNonEmulated(File[] directories) {
+ return Stream.of(directories)
+ .withoutNulls()
+ .filterNot(f -> f.getAbsolutePath().contains("emulated"))
+ .limit(1)
+ .findSingle()
+ .orElse(null);
+ }
+
private static File getSignalStorageDir() throws NoExternalStorageException {
final File storage = Environment.getExternalStorageDirectory();
@@ -135,6 +250,10 @@ public static boolean canWriteInSignalStorageDir() {
return storage.canWrite();
}
+ public static File getLegacyBackupDirectory() throws NoExternalStorageException {
+ return getSignalStorageDir();
+ }
+
public static boolean canWriteToMediaStore() {
return Build.VERSION.SDK_INT > 28 ||
Permissions.hasAll(AppDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index 7482d75caa2..4382078a556 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -192,6 +192,31 @@ public class TextSecurePreferences {
private static final String ARGON2_TESTED = "argon2_tested";
+ //---------------------------------------------------------------------------
+ // JW: added strings are in this block.
+ // true = passphrase, false = Android lock or fingerprint
+ public static final String PROTECTION_METHOD_PREF = "pref_signal_protection_method";
+ // true = backup to removable SD card (if available), false = backup to internal sd card
+ public static final String BACKUP_LOCATION_REMOVABLE_PREF = "pref_backup_location_external";
+ // false (default) means the backup location is not changed by the user, true means it is changed.
+ // This is used to determine at first app start to locate the app backup.
+ public static final String BACKUP_LOCATION_CHANGED = "pref_backup_location_changed";
+ // added to use encrypted zipfiles to store raw backups
+ public static final String BACKUP_STORE_ZIPFILE_PREF = "pref_backup_zipfile";
+ // added to use encrypted zipfiles to store plaintext backups
+ public static final String BACKUP_STORE_ZIPFILE_PLAIN_PREF = "pref_backup_zipfile_plain";
+ // used to see if we delete view once messages after view or not
+ public static final String KEEP_VIEW_ONCE_MESSAGES = "pref_keep_view_once_messages";
+ // used to see if we ignore remote delete messages or not
+ public static final String IGNORE_REMOTE_DELETE = "pref_ignore_remote_delete";
+ // select map type for location picker
+ public static final String GOOGLE_MAP_TYPE = "pref_google_map_type";
+ // delete only media, not the rest of the message, from the All media screen
+ public static final String DELETE_MEDIA_ONLY = "pref_delete_media_only";
+ // who can add you to groups
+ public static final String WHO_CAN_ADD_YOU_TO_GROUPS = "pref_who_can_add_you_to_groups";
+ //---------------------------------------------------------------------------
+
private static final String[] booleanPreferencesToBackup = {SCREEN_SECURITY_PREF,
INCOGNITO_KEYBORAD_PREF,
ALWAYS_RELAY_CALLS_PREF,
@@ -207,7 +232,13 @@ public class TextSecurePreferences {
NEW_CONTACTS_NOTIFICATIONS,
SHOW_INVITE_REMINDER_PREF,
SYSTEM_EMOJI_PREF,
- ENTER_SENDS_PREF};
+ ENTER_SENDS_PREF,
+ // JW: added boolean options
+ BACKUP_STORE_ZIPFILE_PREF,
+ BACKUP_STORE_ZIPFILE_PLAIN_PREF,
+ KEEP_VIEW_ONCE_MESSAGES,
+ IGNORE_REMOTE_DELETE,
+ DELETE_MEDIA_ONLY};
private static final String[] stringPreferencesToBackup = {LED_COLOR_PREF,
LED_BLINK_PREF,
@@ -215,7 +246,10 @@ public class TextSecurePreferences {
NOTIFICATION_PRIVACY_PREF,
THEME_PREF,
LANGUAGE_PREF,
- MESSAGE_BODY_TEXT_SIZE_PREF};
+ MESSAGE_BODY_TEXT_SIZE_PREF,
+ // JW: added String options
+ GOOGLE_MAP_TYPE,
+ WHO_CAN_ADD_YOU_TO_GROUPS};
private static final String[] stringSetPreferencesToBackup = {MEDIA_DOWNLOAD_MOBILE_PREF,
MEDIA_DOWNLOAD_WIFI_PREF,
@@ -1167,4 +1201,88 @@ private static void notifyUnregisteredReceived(Context context) {
public enum MediaKeyboardMode {
EMOJI, STICKER, GIF
}
+
+ //---------------------------------------------------------------------------
+ // JW: added get/set methods are in this block.
+ public static boolean isProtectionMethodPassphrase(@NonNull Context context) {
+ return getBooleanPreference(context, PROTECTION_METHOD_PREF, false);
+ }
+
+ public static void setProtectionMethod(Context context, boolean value) {
+ setBooleanPreference(context, PROTECTION_METHOD_PREF, value);
+ }
+
+ public static void setBackupLocationRemovable(Context context, boolean value) {
+ setBooleanPreference(context, BACKUP_LOCATION_REMOVABLE_PREF, value);
+ }
+ // Default to false so default does the same as official Signal.
+ public static boolean isBackupLocationRemovable(Context context) {
+ return getBooleanPreference(context, BACKUP_LOCATION_REMOVABLE_PREF, false);
+ }
+
+ public static void setBackupLocationChanged(Context context, boolean value) {
+ setBooleanPreference(context, BACKUP_LOCATION_CHANGED, value);
+ }
+
+ public static boolean isBackupLocationChanged(Context context) {
+ return getBooleanPreference(context, BACKUP_LOCATION_CHANGED, false);
+ }
+
+ public static boolean isRawBackupInZipfile(Context context) {
+ return getBooleanPreference(context, BACKUP_STORE_ZIPFILE_PREF, false);
+ }
+
+ public static void setRawBackupZipfile(Context context, boolean value) {
+ setBooleanPreference(context, BACKUP_STORE_ZIPFILE_PREF, value);
+ }
+
+ public static boolean isPlainBackupInZipfile(Context context) {
+ return getBooleanPreference(context, BACKUP_STORE_ZIPFILE_PLAIN_PREF, false);
+ }
+
+ public static void setPlainBackupZipfile(Context context, boolean value) {
+ setBooleanPreference(context, BACKUP_STORE_ZIPFILE_PLAIN_PREF, value);
+ }
+
+ public static boolean isKeepViewOnceMessages(Context context) {
+ return getBooleanPreference(context, KEEP_VIEW_ONCE_MESSAGES, false);
+ }
+
+ public static void setKeepViewOnceMessages(Context context, boolean value) {
+ setBooleanPreference(context, KEEP_VIEW_ONCE_MESSAGES, value);
+ }
+
+ public static String getGoogleMapType(Context context) {
+ return getStringPreference(context, GOOGLE_MAP_TYPE, "normal");
+ }
+
+ public static void setGoogleMapType(Context context, String value) {
+ setStringPreference(context, GOOGLE_MAP_TYPE, value);
+ }
+
+ public static boolean isIgnoreRemoteDelete(Context context) {
+ return getBooleanPreference(context, IGNORE_REMOTE_DELETE, false);
+ }
+
+ public static void setIgnoreRemoteDelete(Context context, boolean value) {
+ setBooleanPreference(context, IGNORE_REMOTE_DELETE, value);
+ }
+
+ public static boolean isDeleteMediaOnly(Context context) {
+ return getBooleanPreference(context, DELETE_MEDIA_ONLY, false);
+ }
+
+ public static void setDeleteMediaOnly(Context context, boolean value) {
+ setBooleanPreference(context, DELETE_MEDIA_ONLY, value);
+ }
+
+ public static String whoCanAddYouToGroups(Context context) {
+ return getStringPreference(context, WHO_CAN_ADD_YOU_TO_GROUPS, "nonblocked");
+ }
+
+ public static void setWhoCanAddYouToGroups(Context context, String value) {
+ setStringPreference(context, WHO_CAN_ADD_YOU_TO_GROUPS, value);
+ }
+ // End added methods block
+ //---------------------------------------------------------------------------
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UriUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/UriUtils.java
new file mode 100644
index 00000000000..7f5b786466b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/UriUtils.java
@@ -0,0 +1,119 @@
+// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri
+
+package org.thoughtcrime.securesms.util;
+
+import androidx.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.provider.DocumentsContract;
+import android.content.Context;
+import android.net.Uri;
+import java.io.File;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.util.List;
+
+
+public class UriUtils {
+
+ private static final String PRIMARY_VOLUME_NAME = "primary";
+
+ public static String getFullPathFromTreeUri(Context context, @Nullable final Uri treeUri) {
+ if (treeUri == null) return "";
+ String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), context);
+ if (volumePath == null) return File.separator;
+ if (volumePath.endsWith(File.separator))
+ volumePath = volumePath.substring(0, volumePath.length() - 1);
+
+ String documentPath = getDocumentPathFromTreeUri(treeUri);
+ if (documentPath.endsWith(File.separator))
+ documentPath = documentPath.substring(0, documentPath.length() - 1);
+
+ if (documentPath.length() > 0) {
+ if (documentPath.startsWith(File.separator))
+ return volumePath + documentPath;
+ else
+ return volumePath + File.separator + documentPath;
+ }
+ else return volumePath;
+ }
+
+ private static String getVolumePath(final String volumeId, Context context) {
+ if (Build.VERSION.SDK_INT < 21)
+ return null;
+ if (Build.VERSION.SDK_INT >= 30)
+ return getVolumePathForAndroid11AndAbove(volumeId, context);
+ else
+ return getVolumePathBeforeAndroid11(volumeId, context);
+ }
+
+ private static String getVolumePathBeforeAndroid11(final String volumeId, Context context){
+ try {
+ StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+ Class> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
+ Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
+ Method getUuid = storageVolumeClazz.getMethod("getUuid");
+ Method getPath = storageVolumeClazz.getMethod("getPath");
+ Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
+ Object result = getVolumeList.invoke(mStorageManager);
+
+ final int length = Array.getLength(result);
+ for (int i = 0; i < length; i++) {
+ Object storageVolumeElement = Array.get(result, i);
+ String uuid = (String) getUuid.invoke(storageVolumeElement);
+ Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
+
+ if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) // primary volume?
+ return (String) getPath.invoke(storageVolumeElement);
+
+ if (uuid != null && uuid.equals(volumeId)) // other volumes?
+ return (String) getPath.invoke(storageVolumeElement);
+ }
+ // not found.
+ return null;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ @TargetApi(30)
+ private static String getVolumePathForAndroid11AndAbove(final String volumeId, Context context) {
+ try {
+ StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+ List storageVolumes = mStorageManager.getStorageVolumes();
+ for (StorageVolume storageVolume : storageVolumes) {
+ // primary volume?
+ if (storageVolume.isPrimary() && PRIMARY_VOLUME_NAME.equals(volumeId))
+ return storageVolume.getDirectory().getPath();
+
+ // other volumes?
+ String uuid = storageVolume.getUuid();
+ if (uuid != null && uuid.equals(volumeId))
+ return storageVolume.getDirectory().getPath();
+
+ }
+ // not found.
+ return null;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ @TargetApi(21)
+ private static String getVolumeIdFromTreeUri(final Uri treeUri) {
+ final String docId = DocumentsContract.getTreeDocumentId(treeUri);
+ final String[] split = docId.split(":");
+ if (split.length > 0) return split[0];
+ else return null;
+ }
+
+ @TargetApi(21)
+ private static String getDocumentPathFromTreeUri(final Uri treeUri) {
+ final String docId = DocumentsContract.getTreeDocumentId(treeUri);
+ final String[] split = docId.split(":");
+ if ((split.length >= 2) && (split[1] != null)) return split[1];
+ else return File.separator;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
index b345b92dcc1..cdceebc76b6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
@@ -350,20 +350,8 @@ public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size
* Takes into account both the build age as well as any remote deprecation values.
*/
public static long getTimeUntilBuildExpiry(long currentTime) {
- if (SignalStore.misc().isClientDeprecated()) {
- return 0;
- }
-
- long buildAge = currentTime - BuildConfig.BUILD_TIMESTAMP;
- long timeUntilBuildDeprecation = BUILD_LIFESPAN - buildAge;
- long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation(currentTime);
-
- if (timeUntilRemoteDeprecation != -1) {
- long timeUntilDeprecation = Math.min(timeUntilBuildDeprecation, timeUntilRemoteDeprecation);
- return Math.max(timeUntilDeprecation, 0);
- } else {
- return Math.max(timeUntilBuildDeprecation, 0);
- }
+ // JW never expire builds. This is an ugly hack but it prevents me from making changes all over the code with each new release.
+ return Integer.MAX_VALUE;
}
public static T getRandomElement(T[] elements) {
diff --git a/app/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.webp b/app/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.webp
new file mode 100644
index 00000000000..51776625b02
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-hdpi/ic_lock_white_24dp.webp b/app/src/main/res/drawable-hdpi/ic_lock_white_24dp.webp
new file mode 100644
index 00000000000..b3ef62a6c1c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_lock_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-hdpi/whatsapp.png b/app/src/main/res/drawable-hdpi/whatsapp.png
new file mode 100644
index 00000000000..fda523a99fb
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/whatsapp.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_content_copy_white_24dp.webp b/app/src/main/res/drawable-mdpi/ic_content_copy_white_24dp.webp
new file mode 100644
index 00000000000..8012eb076e7
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_content_copy_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-mdpi/ic_lock_white_24dp.webp b/app/src/main/res/drawable-mdpi/ic_lock_white_24dp.webp
new file mode 100644
index 00000000000..e8506ad5307
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_lock_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-mdpi/whatsapp.png b/app/src/main/res/drawable-mdpi/whatsapp.png
new file mode 100644
index 00000000000..295557dd527
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/whatsapp.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.webp b/app/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.webp
new file mode 100644
index 00000000000..79fc035f43e
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_lock_white_24dp.webp b/app/src/main/res/drawable-xhdpi/ic_lock_white_24dp.webp
new file mode 100644
index 00000000000..e4c0c95f828
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_lock_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-xhdpi/whatsapp.png b/app/src/main/res/drawable-xhdpi/whatsapp.png
new file mode 100644
index 00000000000..e69ab75969e
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/whatsapp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.webp b/app/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.webp
new file mode 100644
index 00000000000..a47105458d4
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_lock_white_24dp.webp b/app/src/main/res/drawable-xxhdpi/ic_lock_white_24dp.webp
new file mode 100644
index 00000000000..efb7f01a753
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_lock_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-xxhdpi/whatsapp.png b/app/src/main/res/drawable-xxhdpi/whatsapp.png
new file mode 100644
index 00000000000..ca10482946d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/whatsapp.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_content_copy_white_24dp.webp b/app/src/main/res/drawable-xxxhdpi/ic_content_copy_white_24dp.webp
new file mode 100644
index 00000000000..81ae8168ec5
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_content_copy_white_24dp.webp differ
diff --git a/app/src/main/res/drawable-xxxhdpi/whatsapp.png b/app/src/main/res/drawable-xxxhdpi/whatsapp.png
new file mode 100644
index 00000000000..e863641daa7
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/whatsapp.png differ
diff --git a/app/src/main/res/layout/activity_place_picker.xml b/app/src/main/res/layout/activity_place_picker.xml
index e1ff0d29ce2..53a28494f4c 100644
--- a/app/src/main/res/layout/activity_place_picker.xml
+++ b/app/src/main/res/layout/activity_place_picker.xml
@@ -29,6 +29,38 @@
app:layout_constraintVertical_bias="0.0"
app:backgroundColor="@color/signal_colorBackground"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/import_export_fragment.xml b/app/src/main/res/layout/import_export_fragment.xml
new file mode 100644
index 00000000000..a53085f1f16
--- /dev/null
+++ b/app/src/main/res/layout/import_export_fragment.xml
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/text_secure_normal.xml b/app/src/main/res/menu/text_secure_normal.xml
index e0b6605bfd1..0e6ca9885e2 100644
--- a/app/src/main/res/menu/text_secure_normal.xml
+++ b/app/src/main/res/menu/text_secure_normal.xml
@@ -20,6 +20,10 @@
android:id="@+id/menu_clear_unread_filter"
android:visible="false" />
+
+
+
diff --git a/app/src/main/res/values-ar/strings1.xml b/app/src/main/res/values-ar/strings1.xml
new file mode 100644
index 00000000000..776ddd9ff42
--- /dev/null
+++ b/app/src/main/res/values-ar/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ استيراد / تصدير
+
+ استعادة نسخة احتياطية مشفرة؟
+
+ استرجاع نسخة احتياطية مشفرة يؤدي إلى استبدال المفاتيح حالية والإعدادات والرسائل. المعلومات الحالية في سيجنال والغير موجودة في النسخة الاحتياطية سوف تفقد.
+ استعادة
+ الاستعادة جارٍ...
+ استعادة النسخ الاحتياطية المشفرة جارٍ..
+ لم يتم العثور على نسخة احتياطية مشفرة!
+ تمت الاستعادة!
+
+ تصديرإلى كرت الذاكرة؟
+ هذه الخاصية تصدر مفاتيحك المشفرة و إعداداتك و رسائلك لكرت الذاكرة.
+ جاري تصدير مفاتيحك المشفرة و إعداداتك و رسائلك
+
+ استعادة النسخ الاحتياطية المشفرة
+ تصدير نسخة احتياطية غير مشفرة لكرت الذاكرة...
+ استعادة نسخة احتياطية مشفرة
+ استعادة نسخة احتياطية مشفرة من سيجنال.
+ استيراد قاعدة بيانات رسائل SMS بالنظام
+ استيراد قاعدة البيانات من تطبيق الرسائل الافتراضي لنظام التشغيل
+ استيراد نسخة احتياطية غير مشفرة
+ استيراد ملف plaintext كنسخة احتياطية. متوافق مع \'SMS Backup & Restore.\'
+
diff --git a/app/src/main/res/values-be/strings1.xml b/app/src/main/res/values-be/strings1.xml
new file mode 100644
index 00000000000..58158db0402
--- /dev/null
+++ b/app/src/main/res/values-be/strings1.xml
@@ -0,0 +1,5 @@
+
+
+
+ Імпарт / экспарт
+
diff --git a/app/src/main/res/values-bg/strings1.xml b/app/src/main/res/values-bg/strings1.xml
new file mode 100644
index 00000000000..6d2945cf122
--- /dev/null
+++ b/app/src/main/res/values-bg/strings1.xml
@@ -0,0 +1,28 @@
+
+
+
+ Внасяне / изнасяне
+
+ Възстанови от шифровано архивно копие?
+
+Възстановяването от криптиран архив напълно ще промени съществуващите ключове, настройки и съобщения.
+Ще изгубите всичката информация, която е в сегашната инсталация на Сигнал, но не и в криптирания архив.
+ Възстанови
+ Възстановяване
+ Възстановяване на шифрования архив...
+ Не е намерен шифрован архив!
+ Възстановяването приключи!
+
+ Експорт на SD карта?
+ Това⏎\nще изнесе вашите ключове, настройки и съобщения на SD картата.⏎
+ Изнасяне на криптираните ключове,⏎\nнастройки, и съобщения...⏎
+
+ Изнеси криптирания архив
+ Изнеси криптирания⏎\nархив към SD карта.⏎
+ Възстанови от криптирано архивно копие?
+ Възстановяване на криптиран Сигнал архив, който сте изнесли преди
+ Внасяне на всички вече съществуващи SMS-и
+ Внасяне на базата данни от системната програма за съобщения.
+ Внасяне на некриптиран архив
+ Внасяне на некриптиран архив съвместим с \'SMS изнасяне & внасяне\' върху външната памет
+
diff --git a/app/src/main/res/values-ca/strings1.xml b/app/src/main/res/values-ca/strings1.xml
new file mode 100644
index 00000000000..2d1545e86d9
--- /dev/null
+++ b/app/src/main/res/values-ca/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Importació i exportació
+
+ Voleu restablir la còpia de seguretat xifrada?
+
+En restablir una còpia de seguretat xifrada, es reemplaçaran totes les claus, preferències i
+missatges. Perdreu qualsevol informació que estigui al Signal però no a la còpia
+de seguretat.
+ Restaura
+ S\'està restaurant
+ S\'està restaurant la còpia de seguretat xifrada...
+ No s\'ha trobat cap còpia de seguretat xifrada.
+ Restauració completa!
+
+ Exportar a la targeta SD?
+ Això exportarà les teves claus encriptades, ajustaments, i missatges a la targeta SD.
+ Exportant claus encriptades, ajustaments, i missatges...
+
+ Exporta còpia de seguretat encriptada
+ Exporta una còpia de seguretat\nencriptada a la targeta SD.
+ Recupera la còpia de seguretat xifrada
+ Recupera una còpia de seguretat xifrada del Signal exportada prèviament
+ Importa la base de dades d\'SMS del sistema
+ Importa la base de dades del programa de missatgeria predeterminat del sistema
+ Importa la còpia de seguretat en text net
+ Importa una còpia de seguretat en text pla. Compatible amb la «Còpia de seguretat i restauració amb SMS».
+
diff --git a/app/src/main/res/values-cs/strings1.xml b/app/src/main/res/values-cs/strings1.xml
new file mode 100644
index 00000000000..e2d37da1fc5
--- /dev/null
+++ b/app/src/main/res/values-cs/strings1.xml
@@ -0,0 +1,28 @@
+
+
+
+ Import / export
+
+ Obnovit šifrovanou zálohu?
+
+Obnova ze šifrované zálohy kompletně nahradí vaše aktuální klíče, nastavení a zprávy.
+Přijdete o všechny informace z vaší aktuální instalace Signal které nejsou zálohovány.
+ Obnovení
+ Obnovuji
+ Obnovování zašifrované zálohy...
+ Žádná zašifrovaná záloha nebyla nalezena!
+ Obnova dokončena!
+
+ Exportovat na SD kartu?
+ Tímto exportujete šifrované klíče, nastavení a zprávy na SD kartu.
+ Exportování zašifrovaných klíčů, nastavení a zpráv...
+
+ Export šifrované zálohy
+ Exportovat zašifrovanou zálohu na SD kartu.
+ Obnovit šifrovanou zálohu
+ Obnovit dříve exportovanou zašifrovanou zálohu Signal.
+ Import systémové databáze SMS
+ Importovat databázi z výchozí systémové komunikační aplikace
+ Importovat nešifrovanou zálohu
+ Importovat nešifrovanou zálohu kompatibilní s \"SMS Backup & Restore\"
+
diff --git a/app/src/main/res/values-da/strings1.xml b/app/src/main/res/values-da/strings1.xml
new file mode 100644
index 00000000000..e6923a7162b
--- /dev/null
+++ b/app/src/main/res/values-da/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ Importér / eksportér
+
+ Gendan en krypteret backup?
+
+Gendannelse af en krypteret backup vil overskrive eksisterende nøgler, indstillinger og beskeder. Du vil miste alt information i Signal, som ikke findes i din backup.
+ Gendan
+ Gendanner
+ Gendanner krypteret backup...
+ Der blev ikke fundet en krypteret backup!
+ Gendannelse fuldført!
+
+ Eksporter til SD-kort?
+ Dette vil eksportere dine krypteringsnøgler, indstillinger og beskeder til SD-kortet.
+ Eksporterer krypterede nøgler, indstillinger og beskeder...
+
+ Eksporter krypteret backup
+ Eksporter en krypteret backup til SD-kort.
+ Gendan krypteret backup
+ Gendan en krypteret Signal-backup, som blev eksporteret tidligere.
+ Importér system SMS databasen
+ Importér databasen fra systemets standard SMS applikation
+ Importér klartekst backup
+ Importér en klartekst backup fil. Kompatibel med \"SMS Backup & Gendan\"
+
diff --git a/app/src/main/res/values-de/strings1.xml b/app/src/main/res/values-de/strings1.xml
new file mode 100644
index 00000000000..e5cecd30427
--- /dev/null
+++ b/app/src/main/res/values-de/strings1.xml
@@ -0,0 +1,36 @@
+
+
+
+ Datensicherung
+
+ OLED Dunkel
+ Grün Hell
+
+ Passphrase verwenden
+ Passphrase zum Schutz von Signal verwenden anstatt Androids Bildschirmsperre oder Fingerabdruck
+
+ Speicherort für Datensicherungen
+ Unterhaltungen auf SD-Karte sichern (falls vorhanden)
+
+ Verschlüsselte Sicherung wiederherstellen?
+
+Das Wiederherstellen einer verschlüsselten Datensicherung ersetzt deine bestehenden Schlüssel, Einstellungen und Nachrichten. Du wirst jegliche Informationen deiner jetzigen Signal-Installation verlieren, die nicht in dieser Datensicherung enthalten sind.
+ Wiederherstellen
+ Wiederherstellen
+ Verschlüsselte Datensicherung wird wiederhergestellt …
+ Keine verschlüsselte Datensicherung gefunden!
+ Wiederherstellung abgeschlossen!
+
+ Auf SD-Karte exportieren?
+ Dies wird Ihre verschlüsselten Schlüssel, Einstellungen und Nachrichten auf die SD-Karte exportieren.
+ Exportiere verschlüsselte Schlüssel,\nEinstellungen und Nachrichten...
+
+ Verschlüsselte TextSecure-Datensicherung exportieren
+ Exportiere eine verschlüsselte TextSecure-Datensicherung auf die SD-Karte.
+ Verschlüsselte Sicherung wiederherstellen
+ Eine zuvor exportierte, verschlüsselte Signal-Datensicherung wiederherstellen
+ System-SMS-Datenbank importieren
+ Datenbank aus der Standard-SMS-App importieren
+ Klartextsicherung importieren
+ Eine mit »SMS Backup & Restore« kompatible Klartextsicherung importieren
+
diff --git a/app/src/main/res/values-el/strings1.xml b/app/src/main/res/values-el/strings1.xml
new file mode 100644
index 00000000000..093cb25a1a8
--- /dev/null
+++ b/app/src/main/res/values-el/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ Εισαγωγή / εξαγωγή
+
+ Επαναφορά του κρυπτογραφημένου αντίγραφου ασφαλείας;
+
+Η επαναφορά του κρυπτογραφημένου αντίγραφου ασφαλείας θα αντικαταστήσει πλήρως τα υπάρχοντα κλειδιά, τις ρυθμίσεις και τα μηνύματά σου. Θα χάσεις όλα τα δεδομένα αυτής της εγκατάστασης του Signal, αλλά όχι το αντίγραφο ασφαλείας.
+ Επαναφορά
+ Επαναφορά
+ Επαναφορά κρυπτογραφημένου αντίγραφου ασφαλείας...
+ Δε βρέθηκε κρυπτογραφημένο αντίγραφο ασφαλείας!
+ Η επαναφορά ολοκληρώθηκε!
+
+ Εξαγωγή Σε Κάρτα Μνήμης;
+ Αυτό\n θα εξάγει τα κρυπτογραφημένα κλειδιά, τις ρυθμίσεις και τα μηνύματά σας στη κάρτα μνήμης.\n
+ Γίνεται εξαγωγή κρυπτογραφημένων κλειδιών,\n ρυθμίσεων και μηνυμάτων...\n
+
+ Εξαγωγή Κρυπτογραφημένου Αντιγράφου Ασφαλείας
+ Κάνετε εξαγωγή κρυπτογραφημένου\n αντιγράφου ασφαλείας στην κάρτα μνήμης.\n
+ Επαναφορά κρυπτογραφημένου αντίγραφου ασφαλείας
+ Επαναφορά ενός υφιστάμενου κρυπτογραφημένου αντίγραφου ασφαλείας του Signal
+ Εισαγωγή της βάσης δεδομένων SMS του συστήματος
+ Εισαγωγή της βάσης δεδομένων από την προκαθορισμένη συσκευή μηνυμάτων του συστήματος
+ Εισαγωγή μη κρυπτογραφημένου αντίγραφου ασφαλείας
+ Εισαγωγή μη κρυπτογραφημένου αντίγραφου ασφαλείας. Συμβατό με το \'SMS Backup & Restore.\'
+
diff --git a/app/src/main/res/values-es/strings1.xml b/app/src/main/res/values-es/strings1.xml
new file mode 100644
index 00000000000..f7e852ed0b2
--- /dev/null
+++ b/app/src/main/res/values-es/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Importar/exportar
+
+ ¿Restaurar copia de seguridad cifrada?
+
+Restaurar una copia de seguridad cifrada reemplazará completamente sus claves existentes, preferencias, y
+mensajes. Perderá cualquier información que esté en su instalación actual de Signal pero no
+en la copia de seguridad.
+ Restaurar
+ Restaurando
+ Restaurando copia de seguridad cifrada...
+ ¡No se encontró la copia de seguridad cifrada!
+ ¡Restauración completada!
+
+ ¿Exportar a tarjeta SD?
+ Esto\n exportará sus claves cifradas, configuración y mensajes a la tarjeta SD.\n
+ Exportando claves cifradas,\n configuración y mensajes...\n
+
+ Exportar copia de seguridad cifrada
+ Exportar copia de seguridad\n cifrada a la tarjeta SD.\n
+ Restaurar copia de seguridad encriptada
+ Recuperar una copia de seguridad ecriptada de Signal previamente exportada
+ Importar base de datos de SMS del sistema
+ Importar la base de datos desde la aplicación de mensajería por defecto del sistema
+ Importar copia de seguridad en texto plano
+ Importa una copia de seguridad desde un archivo en texto plano. Compatible con la aplicación «SMS Backup & Restore».
+
diff --git a/app/src/main/res/values-et/strings1.xml b/app/src/main/res/values-et/strings1.xml
new file mode 100644
index 00000000000..7e9e722e3c5
--- /dev/null
+++ b/app/src/main/res/values-et/strings1.xml
@@ -0,0 +1,9 @@
+
+
+
+ Impordi / ekspordi
+ Impordi süsteemi SMS-andmebaas
+ Impordi andmebaas vaikimisi süsteemi sõnumirakendusest
+ Impordi lihttekstiline varukoopia
+ Impordi lihttekstiline varukoopia. Ühildub valikuga \"SMS-varundus ja taastamine\".
+
diff --git a/app/src/main/res/values-eu/strings1.xml b/app/src/main/res/values-eu/strings1.xml
new file mode 100644
index 00000000000..32d934a1a14
--- /dev/null
+++ b/app/src/main/res/values-eu/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Inportatu / esportatu
+ Inportatu sistemaren SMS datubasea
+ Inportatu sistemako mezulari aplikazio lehenetsiaren datu-basea.
+ Inportatu zifratu gabeko babeskopia
+ Inportatu \'SMSBackup & Restore\' aukerarekin bateragarria den enkriptatu gabeko babeskopia bat.
+
diff --git a/app/src/main/res/values-fa/strings1.xml b/app/src/main/res/values-fa/strings1.xml
new file mode 100644
index 00000000000..4f216e79fd1
--- /dev/null
+++ b/app/src/main/res/values-fa/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ وارد کردن / خروجی گرفتن
+ وارد کردن پایگاه داده پیامک سیستم
+ وارد کردن پایگاه داده از برنامهٔ پیامرسان پیشفرض سیستم
+ وارد کردن پشتیبان متن ساده
+ وارد کردن یک فایل پشتیبان متن ساده. سازگار با «پشتیبان پیامک و بازگردانی.»
+
diff --git a/app/src/main/res/values-fi/strings1.xml b/app/src/main/res/values-fi/strings1.xml
new file mode 100644
index 00000000000..ddaaf885def
--- /dev/null
+++ b/app/src/main/res/values-fi/strings1.xml
@@ -0,0 +1,28 @@
+
+
+
+ Varmuuskopiot
+
+ Palautetaanko salattu varmuuskopio?
+
+Salatun varmuuskopion palauttaminen korvaa kaikki olemassa olevat avaimet, asetukset ja
+viestit. Menetät kaikki Signalissa olevat tiedot, jotka eivät ole osa varmuuskopiota.
+ Palauta
+ Palautetaan
+ Palautetaan salattua varmuuskopiota...
+ Salattua varmuuskopiota ei löytynyt!
+ Palautus suoritettu!
+
+ Viedäänkö SD-muistikortille?
+ Tämä toiminto \ntulee viemään salatut avaimet, asetukset ja viestit SD-muistikortille.
+ Viedään salattuja avaimia,\nasetuksia ja viestejä...
+
+ Vie salattu varmuuskopio
+ Vie salattu varmuuskopio\nSD-muistikortille.
+ Palauta salattu varmuuskopio
+ Palauta aiemmin viety salattu Signal-varmuuskopio.
+ Tuo järjestelmän tekstiviestit
+ Tuo tietokanta järjestelmän oletustekstiviestisovelluksesta Signaliin.
+ Tuo salaamaton varmuuskopio
+ Tuo salaamaton varmuuskopiotiedosto. Yhteensopiva \"SMS Backup And Restore\" -sovelluksen kanssa.
+
diff --git a/app/src/main/res/values-fr/strings1.xml b/app/src/main/res/values-fr/strings1.xml
new file mode 100644
index 00000000000..feb3293f7b8
--- /dev/null
+++ b/app/src/main/res/values-fr/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Importer / exporter
+
+ OLED Sombre
+ Vert Clair
+
+ Restaurer une sauvegarde chiffrée ?
+ La restauration d\'une sauvegarde chiffrée va complètement remplacer vos clés, préférences et messages. Vous perdrez toutes les informations qui se trouvent dans votre installation Signal actuelle mais pas dans la sauvegarde.
+ Restaurer
+ Restauration
+ Restauration de la sauvegarde chiffrée...
+ Aucune sauvegarde chiffrée trouvée!
+ Restauration terminée !
+
+ Exporter vers la carte SD ?
+ Ceci ⏎\nexportera vos clés chiffrées, les paramètres et les messages sur la carte SD.
+ Exportation des clés chiffrées, \nparamètres et messages ...
+
+ Exporter une sauvegarde chiffrée
+ Exporter une sauvegarde chiffrée vers l\'espace de stockage
+ Restaurer la sauvegarde chiffrée
+ Restaurer une sauvegarde chiffrée précédemment avec Signal
+ Importer la base de données de textos du système
+ Importer la base de données de l’appli de messagerie par défaut du système
+ Importer une sauvegarde en clair
+ Importer un fichier de sauvegarde en clair. Compatible avec « SMS Backup & Restore »
+
diff --git a/app/src/main/res/values-ga/strings1.xml b/app/src/main/res/values-ga/strings1.xml
new file mode 100644
index 00000000000..0db86f229ea
--- /dev/null
+++ b/app/src/main/res/values-ga/strings1.xml
@@ -0,0 +1,5 @@
+
+
+
+ Tabhair isteach / amach
+
diff --git a/app/src/main/res/values-gl/strings1.xml b/app/src/main/res/values-gl/strings1.xml
new file mode 100644
index 00000000000..528a949c304
--- /dev/null
+++ b/app/src/main/res/values-gl/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importar/exportar
+ Importar base de datos de SMS do sistema
+ Importa a base de datos da aplicación de mensaxaría do sistema
+ Importar copia de seguranza en texto simple
+ Importa un ficheiro en texto simple. É compatible con \'Copia de SMS & Restaurar\'.
+
diff --git a/app/src/main/res/values-he/strings1.xml b/app/src/main/res/values-he/strings1.xml
new file mode 100644
index 00000000000..b0adef57fb4
--- /dev/null
+++ b/app/src/main/res/values-he/strings1.xml
@@ -0,0 +1,11 @@
+
+
+
+ לייצא לכרטיס ה-SD?
+ פעולה זו\nתייצא את המפתחות, ההגדרות וההודעות המוצפנות שלך לכרטיס ה-SD.\n
+ מייצא מפתחות, הגדרות\nוהודעות מוצפנות...\n
+
+
+ יצא גיבוי מוצפן
+ יצא גיבוי\nמוצפן לכרטיס ה-SD.\n
+
diff --git a/app/src/main/res/values-hr/strings1.xml b/app/src/main/res/values-hr/strings1.xml
new file mode 100644
index 00000000000..13ffaf3a321
--- /dev/null
+++ b/app/src/main/res/values-hr/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Uvoz / izvoz
+ Uvoz SMS baze podataka sustava
+ Uvoz baze podataka iz zadane aplikacije za poruke
+ Uvoz sigurnosne kopije običnog teksta
+ Uvezi sigurnosnu kopiju običnog teksta. Kompatibilno s \'SMSBackup And Restore\'.
+
diff --git a/app/src/main/res/values-hu/strings1.xml b/app/src/main/res/values-hu/strings1.xml
new file mode 100644
index 00000000000..729cc31bc23
--- /dev/null
+++ b/app/src/main/res/values-hu/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importálás / exportálás
+ Rendszer SMS adatbázis importálása
+ Adatbázis importálása a rendszer alapértelmezett üzenetküldő alkalmazásából
+ Titkosítatlan biztonsági mentés importálása
+ Titkosítatlan biztonságimentés-fájl importálása. Kompatibilis az \'SMS Backup & Restore\'-ral.
+
diff --git a/app/src/main/res/values-id/strings1.xml b/app/src/main/res/values-id/strings1.xml
new file mode 100644
index 00000000000..426a681dc5a
--- /dev/null
+++ b/app/src/main/res/values-id/strings1.xml
@@ -0,0 +1,11 @@
+
+
+
+ Export ke Kartu SD?
+ Ini\nakan mengeksport kunci terenkripsi, dan pesan ke kartu SD
+ Mengeksport kunci yang dienkripsi,\npengaturan, dan pesan...
+
+
+ Eksport Cadangan yang Dienkripsi
+ Eksport cadangan dienkripsi\nke kartu SD.
+
diff --git a/app/src/main/res/values-in/strings1.xml b/app/src/main/res/values-in/strings1.xml
new file mode 100644
index 00000000000..32a7f153f57
--- /dev/null
+++ b/app/src/main/res/values-in/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Impor / ekspor
+ Impor sistem basis data SMS
+ Impor basis data dari aplikasi pengiriman pesan standar di sistem
+ Impor cadangan plaintext
+ Impor cadangan plaintext. Kompatibel dengan \'SMS Backup & Restore.\'
+
diff --git a/app/src/main/res/values-it/strings1.xml b/app/src/main/res/values-it/strings1.xml
new file mode 100644
index 00000000000..8b728b0978f
--- /dev/null
+++ b/app/src/main/res/values-it/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ Importa / Esporta
+
+ Ripristinare la copia cifrata?
+
+Il ripristino di un backup criptato sostituirà completamente le chiavi esistenti, le preferenze e i messaggi. Verranno perse tutte le informazioni presenti nell\'installazione attuale di Signal ma non quelle nel backup.
+ Ripristina
+ Ripristino
+ Ripristino copia cifrata...
+ Nessuna copia cifrata trovata!
+ Ripristino completato!
+
+ Esporta alla scheda SD?
+ Verranno\n esportate nella scheda SD le tue chiavi crittografiche, le impostazioni ed i messaggi.\n
+ Esporta le chiavi crittografiche,\nle impostazioni ed i messaggi...
+
+ Esporta il backup cifrato
+ Esporta un backup\ncifrato sulla scheda SD
+ Ripristina backup criptato
+ Ripristina un backup cifrato di Signal precedentemente esportato.
+ Importa gli SMS dal database criptato
+ Importa i messaggi dall\'app di sistema per gli SMS
+ Importare copia in chiaro
+ Importa un file dalla memoria. Compatibile con \'SMS Backup & Restore\'
+
diff --git a/app/src/main/res/values-iw/strings1.xml b/app/src/main/res/values-iw/strings1.xml
new file mode 100644
index 00000000000..e5379709f3d
--- /dev/null
+++ b/app/src/main/res/values-iw/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ ייבא / ייצא
+ ייבא מסד נתונים של מסרונים של המערכת
+ ייבא מסד נתונים מיישום שליח ברירת המחדל של המערכת
+ ייבא גיבוי מטקסט פשוט
+ ייבא קובץ גיבוי מטקסט פשוט. תואם עם \'גיבוי ושחזור של מסרונים\'.
+
diff --git a/app/src/main/res/values-ja/strings1.xml b/app/src/main/res/values-ja/strings1.xml
new file mode 100644
index 00000000000..76a19e41ec8
--- /dev/null
+++ b/app/src/main/res/values-ja/strings1.xml
@@ -0,0 +1,26 @@
+
+
+
+ インポート/エクスポート
+
+ 暗号化されたバックアップを復元しますか?
+ 暗号化されたバックアップを復元すると、キー・設定・メッセージが完全に置き換わります。アプリ内の情報でバックアップにないものはすべて失われます。
+ 復元
+ 復元中…
+ 暗号化されたバックアップ・ファイルをインポート中…
+ 暗号化されたバックアップ・ファイルは見つかりませんでした。
+ 復元完了!
+
+ SDカードへエクスポートしますか?
+ これで、あなたの暗号化された鍵、設定、及びメッセージはSDカードへエクスポートされますが、よろしいですか?
+ 暗号化されたキー、設定及びメッセージをエクスポート中…
+
+ 暗号化されたバックアップ・ファイルをエクスポート
+ SDカードへ暗号化された\nバックアップ・ファイルをエクスポートします
+ 暗号化されたバックアップを復元する
+ 以前にエクスポートされた暗号化されたバックアップを復元する
+ システムのSMSデータベースをインポート
+ 既定のシステムメッセージアプリからデータベースをインポートする
+ テキストのバックアップをインポート
+ テキストのバックアップファイルをインポートします。「SMSのバックアップと復元」と互換性があります。
+
diff --git a/app/src/main/res/values-km/strings1.xml b/app/src/main/res/values-km/strings1.xml
new file mode 100644
index 00000000000..ff0e52fd772
--- /dev/null
+++ b/app/src/main/res/values-km/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ នាំចូុល / នាំចេញ
+ នាំចូលប្រព័ន្ធមូលដ្ឋានទិន្នន័យ SMS
+ នាំចូលមូលដ្ឋានទិន្នន័យពីកម្មវិធីផ្ញើសារលំនាំដើម
+ នាំចូលការបម្រុងទុកជាអក្សរ
+ នាំចូលការបម្រុងទុក។ ដែលត្រូវគ្នាជាមួយនឹង ‘ការបម្រុង និងស្តារ SMS’ ។
+
diff --git a/app/src/main/res/values-kn/strings1.xml b/app/src/main/res/values-kn/strings1.xml
new file mode 100644
index 00000000000..ff89ffe0afe
--- /dev/null
+++ b/app/src/main/res/values-kn/strings1.xml
@@ -0,0 +1,15 @@
+
+
+
+ ಎಸ್.ಡಿ ಕಾರ್ಡ್ಗೆ ರಫ್ತು ಮಾಡಿ?
+ ಇದು\nಏಸ್.ಡಿ ಕಾರ್ಡ್ಗೆ ನಿಮ್ಮ ಗೂಢಲಿಪೀಕರಣಗೊಂಡ ಕೀಲಿಗಳನ್ನು, ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಮತ್ತು ಸಂದೇಶಗಳ ರಫ್ತು ಮಾಡುತ್ತದೆ.
+ ಗೂಢಲಿಪೀಕರಣಗೊಂಡ ಕೀಲಿಗಳನ್ನು,\nಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಮತ್ತು ಸಂದೇಶಗಳನ್ನು ರಫ್ತು ಮಾಡಲಾಗುತ್ತಿದೆ ...
+
+
+ ಗೂಢಲಿಪೀಕರಣಗೊಂಡ ಬ್ಯಾಕ್ಅಪ್ ಅನ್ನು ರಫ್ತು ಮಾಡಿ
+ ಎಸ್.ಡಿ ಕಾರ್ಡ್ಗೆ \nಒಂದು ಗೂಢಲಿಪೀಕರಣಗೊಂಡ ⏎ ಬ್ಯಾಕ್ಅಪ್ ಅನ್ನು ರಫ್ತು ಮಾಡಿ.
+ ಸಿಸ್ಟಂ ಎಸ್ಎಂಎಸ್ ದತ್ತಾಂಶವನ್ನು ಇಂಪೋರ್ಟ್ ಮಾಡಿ
+ ಡೀಫಾಲ್ಟ್ ಸಿಸ್ಟಂ ಮೆಸೆಂಜರ್ ಆ್ಯಪ್ನಿಂದ ಡೇಟಾಬೇಸ್ ಅನ್ನು ಇಂಪೋರ್ಟ್ ಮಾಡಿ
+ ಸರಳಪಠ್ಯದ ಬ್ಯಾಕಪ್ ಅನ್ನು ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ
+ ಸರಳ ಪಠ್ಯದ ಬ್ಯಾಕಪ್ ಫೈಲ್ ಇಂಪೋರ್ಟ್ ಮಾಡಿ. \'ಎಸ್ಎಂಎಸ್ ಬ್ಯಾಕಪ್ & ಮರುಸ್ಥಾಪನೆ\' ಗೆ ಹೊಂದಿಕೊಳ್ಳುವಂತಿದೆ
+
diff --git a/app/src/main/res/values-ko/strings1.xml b/app/src/main/res/values-ko/strings1.xml
new file mode 100644
index 00000000000..1ffce6ac1a8
--- /dev/null
+++ b/app/src/main/res/values-ko/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ 가져오기 / 내보내기
+ 시스템 SMS 가져오기
+ 기본 시스템 SMS 앱의 데이터베이스 가져오기
+ 암호화되지 않은 백업 가져오기
+ 암호화되지 않은 백업을 가져오세요. \'SMS 백업 및 복원\'에 호환됩니다.
+
diff --git a/app/src/main/res/values-ku/strings1.xml b/app/src/main/res/values-ku/strings1.xml
new file mode 100644
index 00000000000..1605f8e746e
--- /dev/null
+++ b/app/src/main/res/values-ku/strings1.xml
@@ -0,0 +1,45 @@
+
+
+
+ Bibin derve
+ Nivîsana tazî bibin embarê derve?
+ Hişyarî, ev ê naveroka peyamên Signal ên te wek nivîsana tazî derveşîne embarê.
+ Betal
+ Tê derveşandin
+ Nivîsana tazî tê derveşandin embarê...
+ Nikarim li embar binivîsim.
+ Nikaribûm li embar binivîsim.
+ Derveşandin çêbû.
+ Îxrackirina kopiya ewleriya nivîsa tazî
+ Îxrackirina kopiya ewleriya nivîsa tazî embarê ku dikare bi \'SMS Backup & Restore\' bikarbîne
+
+ Ji derve bînin
+ Bibin derve
+
+ Databanka SMS a pergal ji derve bînin?
+ Ev ê peyam
+ ji databanka SMS a standar a pergalê ji derve bîne Signalê. Heger te databanka
+ SMS a pergal bere dîsa anî, anîn ê peyam kopî bike.
+
+ Ji derve bînin
+ Betal
+ Kopî wek nivîsa tazî ji derve bînin?
+ Ev ê peyam
+ji kopî wek nivîsa tazî bîne Signalê. Heger te kopî
+bere dîsa anî, anîn ê peyam kopî bike.
+ Ji derve tê anîn
+ Kopî wek nivîsa tazî tê ji derve anîn...
+ Kopiya nivîsa tazî nahat peyda kirin!
+ Ji derve anîna kopî çênebû!
+ Ji derve anîn xelasbû!
+ Ji bo peyamên SMS ji derve anîn, Signal destûra SMS dixwaze, lê ew hat herdemî rakirin. Li \'Settings\' > \'Destûr > \'SMS\' destûr bide.
+ Ji bo anîn SMS, Signal destûra SMS dixwaze
+ Ji bo xwendina embara derve, Signal destûra Embar dixwaze, lê ew hat herdemî rakirin. Li \'Settings\' > \'Destûr\' > \'Embar\' destûr bide.
+ Ji bo xwendina embara derve, Signal destûra embar dixwaze.
+ Ji bo nivîsandin li embara derve, Signal destûra Embar dixwaze, lê ew hat herdemî rakirin. Li \'Settings\' > \'Destûr\' > \'Embar\' destûr bide.
+ Ji bo nivîsandina embara derve re, Signal destûra Embar dixwaze.
+ Danegeha SMS\'ê ya sîstemê lê bar bike
+ Danegehê ji sepana peyaman ya jixweber bîne
+ Kopiya paşkefta nivîsa sade têbixe
+ Pelgeyek paşkeftê ya nivîsa sade têxe. Hevaheng e bi \'Paşkefta SMSê & Vegerandin.\' re.
+
diff --git a/app/src/main/res/values-lg/strings1.xml b/app/src/main/res/values-lg/strings1.xml
new file mode 100644
index 00000000000..3e99d453e71
--- /dev/null
+++ b/app/src/main/res/values-lg/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Tikula / Wereza
+ Yingiza system SMS database
+ Yingiza datbase okuva ku apu ya default system messenger
+ Yingiza backup yo bubaka obuwandike
+ mport a plaintext backup file. Compatible with \'SMS Backup & Restore.\'
+
diff --git a/app/src/main/res/values-lt/strings1.xml b/app/src/main/res/values-lt/strings1.xml
new file mode 100644
index 00000000000..9134a5f048d
--- /dev/null
+++ b/app/src/main/res/values-lt/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importuoti / eksportuoti
+ Importuoti sistemos SMS duomenų bazę
+ Importuoti duomenų bazę iš numatytosios sistemos pokalbių programėlės
+ Importuoti grynojo teksto atsarginę kopiją
+ Importuoti grynojo teksto atsarginės kopijos failą. Suderinama su „SMS atsarginės kopijos darymu ir atkūrimu“.
+
diff --git a/app/src/main/res/values-mk/strings1.xml b/app/src/main/res/values-mk/strings1.xml
new file mode 100644
index 00000000000..93d2fda4d95
--- /dev/null
+++ b/app/src/main/res/values-mk/strings1.xml
@@ -0,0 +1,6 @@
+
+
+ Импортирај / експортирај
+ Импортирај системска SMS база на податоци
+ Импортирајте нешифрирана копија
+
diff --git a/app/src/main/res/values-my/strings1.xml b/app/src/main/res/values-my/strings1.xml
new file mode 100644
index 00000000000..c0fe5822202
--- /dev/null
+++ b/app/src/main/res/values-my/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ တင်သွင်း / ပို့
+ ဖုန်းစနစ်၏ SMS အချက်အလက်များကို ထည့်သွင်းသည်
+ မူရင်းစနစ် မက်ဆင်ဂျာ app မှအချက်အလက်များကို တင်ပို့သည်
+ အရံသိမ်းထားသော စာများကို တင်သွင်းသည်
+ အရံသိမ်းထားသော စာများကို ကူးပြောင်းသည်။ SMS အရန်သိမ်းခြင်း & ပြန်သုံးခြင်းတို့ဖြင့်လိုက်လျောညီထွေဖြစ်သည်။
+
diff --git a/app/src/main/res/values-nb/strings1.xml b/app/src/main/res/values-nb/strings1.xml
new file mode 100644
index 00000000000..a90aac8715a
--- /dev/null
+++ b/app/src/main/res/values-nb/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importer / eksporter
+ Importer systemets SMS-database
+ Importer database fra systemets forvalgte meldingsprogram
+ Importer ukryptert sikkerhetskopi
+ Importer ukryptert sikkerhetskopi. Dette kan brukes med «SMS-sikkerhetskopi & -gjenoppretting.»
+
diff --git a/app/src/main/res/values-nl/strings1.xml b/app/src/main/res/values-nl/strings1.xml
new file mode 100644
index 00000000000..77a46fdf2cd
--- /dev/null
+++ b/app/src/main/res/values-nl/strings1.xml
@@ -0,0 +1,98 @@
+
+
+
+ Importeren / exporteren
+
+ OLED Donker
+ Groen Licht
+ Blauw Licht
+ Locatie
+ Kaartsoort
+ Normaal
+ Hybride
+ Satelliet
+ Terrein
+ Geen
+ Iedereen
+ Alle niet geblokkeerde contacten
+ Alleen contacten
+ Alleen niet geblokkeerde systeem contacten
+ Niemand
+
+ Gebruik een wachtwoord
+ Gebruik een wachtwoord om Signal te beveiligen in plaats van de standaard Android schermbeveiliging of vingerafdruk
+ Het gebruik van de schermbeveiliging vereist Android 5.0 (Lollipop) of hoger.
+ Android versie te laag
+
+ Push meldingen via FCM
+ Ontvang notifications via Google Play Services. Als dit uitstaat wordt een websocket gebruikt, dit kan de accu iets meer belasten
+ Google Play Services niet gevonden
+ Signal moet opnieuw starten om de veranderingen door te voeren
+
+ Beheer verwijderen van berichten
+ Behoud eenmalige weergave-media
+ Deze optie zorgt ervoor dat eenmalige weergave-media behandeld worden als gewone media en niet verwijderd worden na het bekijken
+ Negeer verwijderen op afstand
+ Deze optie voorkomt dat berichten door de verzender verwijderd kunnen worden
+ Verwijder alleen media
+ Verwijder in het Alle media scherm alleen media, niet het bericht zelf
+
+ Backup locatie
+ Backup locatie (tik om aan te passen)
+ Backup chats naar de SD-kaart (indien aanwezig)
+
+ Versleutelde backups in zipfile opslaan
+ De zipfile wordt versleuteld met hetzelfde password als de reguliere backup, of geen indien geen regulierebackup geconfigureerd
+ Onversleutelde backups in zipfile opslaan
+ De zipfile wordt versleuteld met hetzelfde password als de reguliere backup, of geen indien geen regulierebackup geconfigureerd
+
+ Versleutelde backup herstellen?
+
+Herstellen van een versleutelde back-up zal je bestaande sleutels, voorkeuren en berichten volledig vervangen. Je zult alle informatie verliezen die wel in je huidige Signal-installatie zit, maar niet in de back-up.
+ Herstellen
+ Herstellen
+ Versleutelde backup herstellen...
+ Geen versleutelde back-up gevonden!
+ Herstellen voltooid!
+ OK
+ Sms-berichten uit de systeemdatabank importeren?
+ Importeer de databank van de standaardberichtenapp
+ Onversleuteld back-up-bestand importeren
+ Importeer een onversleuteld back-up-bestand. Compatibel met ‘Sms back-up & Herstellen’.
+
+ WhatsApp Backup niet gevonden! Plaats msgstore.db in the root directory van de telefoons interne geheugen.
+ Import WhatsApp backup?
+ Dit importeert berichten uit een niet versleutelde
+ WhatsApp msgstore.db backup in de root van de interne sd kaart.
+ Als je deze database al al eerder geïmporteerd hebt komen de berichten er dubbel in.
+
+ Importeren whatsapp backup...
+ Import WhatsApp backup
+ Importeer een WhatsApp db file. Plaats msgstore.db in de root directory
+ Import media berichten
+ Import groepsgesprekken als er een Signal groep met dezelfde naam bestaat
+ Voorkom dubbele berichten
+
+ Opslaan op SD-kaart?
+ Dit exporteert je keys, instellingen en berichten versleuteld naar de SD-kaart
+ Keys, instellingen en berichten versleuteld naar de SD-kaart aan het exporteren...
+
+ Exporteer Versleutelde Back-up
+ Export een versleutelde back-up naar de SD-kaart.
+ Versleutelde back-up herstellen
+ Herstel een eerder geëxporteerde versleutelde back-up van Signal
+
+ Versleutelde backup terugzetten
+
+ Groep beheer
+ Wie kan je aan groepen toevoegen
+
+ SMS export status uitzetten
+ SMS export status uitzetten
+ Je kunt de SMS export status uitzetten zodat je de SMS berichten opnieuw kunt exporteren
+ Reset export status voor %d berichten
+ Export vlag uitzetten...
+ Verander de export status niet
+ Je kunt SMS berichten opnieuw exporteren vanuit Signal in settings...
+ Hou de export status onveranderd
+
diff --git a/app/src/main/res/values-nn/strings1.xml b/app/src/main/res/values-nn/strings1.xml
new file mode 100644
index 00000000000..b1d5cd3e1df
--- /dev/null
+++ b/app/src/main/res/values-nn/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importer / eksporter
+ Importer systemet sin SMS-database
+ Importer database frå standard meldingsprogram til systemet
+ Importer klartekst-reservekopi
+ Importer klartekst-reservekopi. Dette kan brukast med «SMS Backup & Restore.»
+
diff --git a/app/src/main/res/values-no/strings1.xml b/app/src/main/res/values-no/strings1.xml
new file mode 100644
index 00000000000..627200b8e47
--- /dev/null
+++ b/app/src/main/res/values-no/strings1.xml
@@ -0,0 +1,23 @@
+
+
+
+ Importere / eksportere
+
+ Gjenopprett kryptert backup?
+
+Innlasting av en kryptert backup vil erstatte dine eksisterende nøkler, preferanser og meldinger. Du vil miste informasjonen som finnes i din eksisterende Signal installasjon, men ikke i backupen.
+ Gjenopprett
+ Gjenoppretter
+ Gjenoppretter kryptert backup...
+ Ingen kryptert backup funnet!
+ Gjenoppretting fullført!
+
+ Eksporter til SD-kort?
+ Dette vil eksportere dine krypteringsnøkler, innstillinger og meldinger til SD-kortet.
+ Eksporterer krypterte nøkler, innstillinger og meldinger...
+
+ Eksporter kryptert backup
+ Eksporter en kryptert backup til SD-kortet.
+ Gjenopprett kryptert sikkerhetskopi
+ Gjenopprett en tidligere eksportert og kryptert Signal sikkerhetskopi
+
diff --git a/app/src/main/res/values-pl/strings1.xml b/app/src/main/res/values-pl/strings1.xml
new file mode 100644
index 00000000000..7f796907b1a
--- /dev/null
+++ b/app/src/main/res/values-pl/strings1.xml
@@ -0,0 +1,28 @@
+
+
+
+ Import / eksport
+
+ Przywrócić z zaszyfrowanej kopii zapasowej?
+
+Przywracanie szyfrowanej kopii zapasowej spowoduje nadpisanie Twoich istniejących kluczy, ustawień i wiadomości.
+Stracisz wszystkie informacje obecnie zawarte w aplikacji Signal z wyjątkiem tych zawartych w kopii zapasowej.
+ Przywróć
+ Przywracanie
+ Przywracanie szyfrowanej kopii zapasowej...
+ Nie odnaleziono szyfrowanej kopii zapasowej!
+ Przywracanie zakończone!
+
+ Wyeksportować na kartę SD?
+ Wszystkie zaszyfrowane klucze , ustawienia i wiadomości zostaną wyeksportowane na kartę SD.
+ Eksportowanie zaszyfrowanych kluczy,\nustawień i wiadomości...\n\n
+
+ Eksportowanie szyfrowanej kopii zapasowej
+ Eksportuj szyfrowaną kopię zapasową na kartę SD.
+ Przywróć szyfrowaną kopię zapasową
+ Przywróć poprzednio wyeksportowaną kopię zapasową Signal
+ Importuj systemową bazę danych SMS
+ Importuj wiadomości z domyślnej aplikacji SMS
+ Importuj nieszyfrowaną kopię zapasową
+ Importuj niezaszyfrowany plik kopii zapasowej, kompatybilny z "SMS Backup & Restore".
+
diff --git a/app/src/main/res/values-pt-rBR/strings1.xml b/app/src/main/res/values-pt-rBR/strings1.xml
new file mode 100644
index 00000000000..ec276f87787
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings1.xml
@@ -0,0 +1,30 @@
+
+
+
+ Importar / exportar
+
+ Restaurar backup criptografado?
+
+ Restaurar um backup criptografado irá substituir completamente suas chaves existentes,
+ preferências e mensagens. Você perderá toda informação que estiver em sua
+ instalação Signal atual mas não estiver no backup.
+
+ Restaurar
+ Restaurando
+ Restaurando backup criptografado...
+ Nenhum backup criptografado encontrado!
+ Restauração finalizada!
+
+ Exportar para cartão SD?
+ Isto\n exportará suas chaves criptografadas, configurações e mensagens para o cartão SD.\n
+ Exportando chaves criptogradas,\n configurações e mensagens...\n
+
+ Exportar backup criptografado
+ Exportar um backup\n criptografado para o cartão SD.\n
+ Restaurar backup criptografado
+ Restaurar um backup criptografado do Signal exportado anteriormente
+ Importar base de dados SMS do sistema
+ Importar a base de dados do aplicativo de mensagens padrão do sistema
+ Importar backup de texto não criptografado
+ Importar um backup em texto não criptografado. Compatível com \'Backup & Restauro de SMS.\'
+
diff --git a/app/src/main/res/values-pt/strings1.xml b/app/src/main/res/values-pt/strings1.xml
new file mode 100644
index 00000000000..50d7be5f36d
--- /dev/null
+++ b/app/src/main/res/values-pt/strings1.xml
@@ -0,0 +1,30 @@
+
+
+
+ Importar / Exportar
+
+ Restaurar cópia de segurança encriptada?
+
+ Restaurar de uma cópia de segurança encriptada vai substituir todas as chaves, preferências e
+ mensagens. Irá perder toda a informação na configuração actual do Signal mas
+ não na cópia de segurança.
+
+ Restaurar
+ A restaurar
+ A restaurar cópia de segurança encriptada...
+ Não foi encontrada uma cópia de segurança encriptada!
+ Restauro completo!
+
+ Exportar para cartão SD?
+ As suas\nchaves de segurança, configurações e mensagens serão exportadas para o cartão SD.\n
+ A exportar chaves de segurança,\nconfigurações e mensagens...\n
+
+ A exportar cópia de segurança cifrada
+ Exportar um backup\ncifrado para o cartão SD.\n
+ Restaurar cópia de segurança encriptada
+ Restaurar uma cópia de segurança encriptada do Signal previamente exportada
+ Importar a base de dados de SMS do sistema
+ Importar a base de dados da aplicação padrão de mensagens do sistema.
+ Importar cópia de segurança em texto simples
+ Importar um ficheiro de cópia de segurança em texto simples. Compatível com \'Cópia de segurança de SMS e Restaurar\'.
+
diff --git a/app/src/main/res/values-ro/strings1.xml b/app/src/main/res/values-ro/strings1.xml
new file mode 100644
index 00000000000..10db2d7cddb
--- /dev/null
+++ b/app/src/main/res/values-ro/strings1.xml
@@ -0,0 +1,28 @@
+
+
+
+ Importă / Exportă
+
+ Restaurez backup criptat?
+
+Restaurarea unui backup criptat va înlocui în totalitate cheile, preferințele și mesajele tale existente.
+Vei pierde orice informație care se află în instalarea curentă de Signal și care nu se află în backup.
+ Restaurează
+ Se restaurează
+ Se restaurează backup-ul criptat...
+ Nu a fost găsit nici un backup criptat!
+ Restaurare completă!
+
+ Export pe SD card?
+ Această acțiune va exporta cheile criptate, setările și mesajele pe SD card.
+ Se exportă cheile criptate, \nsetările și mesajele...
+
+ Exportă backup criptat
+ Un backup criptat a fost exportat pe SD card.
+ Restaurează backup criptat?
+ Restaurează un backup criptat exportat anterior cu Signal
+ Import baza de date cu SMS-uri?
+ Importă baza de date din aplicația implicită de mesagerie din sistem
+ Importă backup necriptat
+ Importă un fișier de backup necriptat. Compatibil cu \'SMS Backup & Restore.\'
+
diff --git a/app/src/main/res/values-ru/strings1.xml b/app/src/main/res/values-ru/strings1.xml
new file mode 100644
index 00000000000..194bcf023f7
--- /dev/null
+++ b/app/src/main/res/values-ru/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ Импорт / экспорт
+
+ Восстановить из зашифрованной резервной копии?
+
+Восстановление из зашифрованной резервной копии полностью заменит существующие ключи, настройки и сообщения. Вы потеряете всю информацию, которая сейчас есть в Signal, но которой нет в резервной копии.
+ Восстановить
+ Восстановление
+ Восстанавливаем из зашифрованной резервной копии...
+ Зашифрованная резервная копия не найдена!
+ Восстановление завершено.
+
+ Экспортировать на SD-карту?
+ Вы собираетесь экспортировать на SD-карту свои зашифрованные ключи, настройки и сообщения.
+ Производится экспорт зашифрованных ключей, настроек и сообщений...
+
+ Экспорт зашифрованной резервной копии
+ Экспортировать на SD-карту зашифрованную резервную копию.
+ Восстановление зашифрованной резервной копии
+ Восстановить зашифрованную резервную копию, которая была ранее экспортирована из Signal
+ Импорт системной базы SMS
+ Импортировать сообщения из системного SMS-приложения
+ Импорт незашифрованной резервной копии
+ Импортировать незашифрованную резервную копию. Совместимо с «SMS Backup & Restore».
+
diff --git a/app/src/main/res/values-sk/strings1.xml b/app/src/main/res/values-sk/strings1.xml
new file mode 100644
index 00000000000..dcce0138e48
--- /dev/null
+++ b/app/src/main/res/values-sk/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Import / export
+
+ Obnoviť zašifrovanú zálohu?
+
+Obnovenie šifrovanej zálohy kompletne nahradí existujúce kľúče, nastavenia a
+správy. Stratíte všetky existujúce informácie v aktuálnej inštalácii Signal, okrem
+dát zálohy.
+ Obnovenie
+ Obnovenie
+ Obnovujem šifrovanú zálohu...
+ Nenašiel som šifrovanú zálohu!
+ Obnova dokončená!
+
+ Exportovať na SD kartu?
+ Toto\nexportuje Vaše kryptované kľúče, nastavenia a správy na SD kartu.
+ Exportujem zakryptované kľúče, nastavenia a správy...
+
+ Exportovať zakryptovanú zálohu
+ Exportovať zakryptovanú\nzálohu na SD kartu.
+ Obnoviť šifrovanú zálohu
+ Obnoviť predtým exportovanú šifrovanú zálohu Signal
+ Importovať systémovú databázu SMS
+ Importovať databázu z predvolenej systémovej aplikácie SMS správ
+ Importovať nešifrovanú zálohu
+ Importovať nešifrovaný záložný súbor. Kompatibilné s \"SMS Backup & Restore.\"
+
diff --git a/app/src/main/res/values-sl/strings1.xml b/app/src/main/res/values-sl/strings1.xml
new file mode 100644
index 00000000000..615e3305799
--- /dev/null
+++ b/app/src/main/res/values-sl/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ Uvoz/Izvoz
+
+ Obnovim iz šifirane varnostne kopije?
+
+Uvoz iz šifrirane varnostne kopije bo popolnoma nadomestil obstoječe ključe, nastavitve, in sporočila. Izgubili boste vse podatke iz trenutne namestitve aplikacije Signal, ki niso v tej varnostni kopiji.
+ Obnovi
+ Obnavljam
+ Obnavljam iz šifrirane varnostne kopije...
+ Ne najdem nobene šifrirane varnostne kopije!
+ Obnova dokončana!
+
+ Izvozim na kartico SD?
+ S tem boste izvozili svoje ključe, nastavitve in sporočila na kartico SD v šifrirani obliki.
+ Izvažam ključe, nastavitve in sporočila v šifrirani obliki...
+
+ Izvozi šifrirano varnostno kopijo
+ Izvozi šifrirano varnostno kopijo na kartico SD.
+ Uvoz iz šifrirane varnostne kopije
+ Obnova podatkov iz predhodno ustvarjene šifrirane varnostne kopije Signal.
+ Uvoz iz sistemske zbirke sporočil SMS
+ Uvoz zbirke sporočil iz sistemsko privzete aplikacije SMS.
+ Uvoz iz nešifrirane varnostne kopije
+ Uvoz iz datoteke z nešifrirano varnostno kopijo. Združljivo s sistemskimi varnostnimi kopijami sporočil SMS.
+
diff --git a/app/src/main/res/values-sq/strings1.xml b/app/src/main/res/values-sq/strings1.xml
new file mode 100644
index 00000000000..f4f9f5a016a
--- /dev/null
+++ b/app/src/main/res/values-sq/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Importo / eksporto
+ Importo bazë të dhënash SMS të sistemit
+ Importoni bazën e të dhënave nga aplikacioni parazgjedhje për shkëmbim mesazhesh i sistemit
+ Importo kopjeruajtje tekst të thjeshtë
+ Importoni një kartelë kopjeruajtje tekst të thjeshtë. E përputhshme me hapin \'Kopjeruajtje & Rikthim SMS-sh\'.
+
diff --git a/app/src/main/res/values-sr/strings1.xml b/app/src/main/res/values-sr/strings1.xml
new file mode 100644
index 00000000000..2b08dbe7060
--- /dev/null
+++ b/app/src/main/res/values-sr/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ Увоз/извоз
+ Увези системску СМС базу података
+ Увоз базе података из подразумеване системске апликације за размену порука
+ Увези текстуалну резерву
+ Увоз обичне текстуалне резерве. Компатибилно са апликацијом „SMS Backup & Restore“.
+
diff --git a/app/src/main/res/values-sv/strings1.xml b/app/src/main/res/values-sv/strings1.xml
new file mode 100644
index 00000000000..b239db6f2d8
--- /dev/null
+++ b/app/src/main/res/values-sv/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Importera/exportera
+
+ Återställ krypterad säkerhetskopia?
+
+Återställandet av en krypterad säkerhetskopia kommer helt ersätta dina existerande nycklar, inställningar och
+ meddelanden. Du kommer förlora all information som finns i din nuvarande Signal installation
+som inte finns i säkerhetskopian.
+ Återställ
+ Återställer
+ Återställer krypterad säkerhetskopia...
+ Ingen krypterad säkerhetskopia kunde hittas!
+ Återställningen lyckades!
+
+ Exportera till minneskort?
+ Denna\nåtgärd kommer att exportera dina krypterade nycklar, inställningar och meddelanden till ditt minneskort.
+ Exporterar krypterade nycklar,\ninställningar och meddelanden...
+
+ Exportera krypterad säkerhetskopia
+ Exportera en krypterad\nsäkerhetskopia till minneskortet.
+ Återställ krypterad säkerhetskopia
+ Återställ en tidigare exporterad krypterad säkerhetskopia av Signal
+ Importera systemets SMS-databas
+ Importera databasen från standardappen för meddelanden
+ Importera okrypterad backup
+ Importera en okrypterad säkerhetskopia. Kompatibel med \"SMSBackup & Restore.\"
+
diff --git a/app/src/main/res/values-ta/strings1.xml b/app/src/main/res/values-ta/strings1.xml
new file mode 100644
index 00000000000..2da51e96871
--- /dev/null
+++ b/app/src/main/res/values-ta/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ இறக்குமதி / ஏற்றுமதி
+ தொலைபேசியிலுள்ள SMS-களை இறக்குமதி செய்
+ இயல்புநிலை கணினி தூதர் பயன்பாடு தரவுத்தளத்தை இறக்குமதி செய்க
+ முன்சேமித்த இயல்புஉரை கோப்பிலிருந்து இறக்குமதிசெய்
+ எளிய உரை காப்புப்பதிவு பயனர் தரவு கோப்பை இறக்குமதி செய்க. \'SMS காப்புப்பிரதி & மீட்டமைப்புடன்\' பொருந்தக்கூடியது.
+
diff --git a/app/src/main/res/values-te/strings1.xml b/app/src/main/res/values-te/strings1.xml
new file mode 100644
index 00000000000..9a3f76a5f3a
--- /dev/null
+++ b/app/src/main/res/values-te/strings1.xml
@@ -0,0 +1,42 @@
+
+
+
+ ఎగుమతి
+
+ ఎగుమతి
+ సహజమైన వాక్యాలను నిల్వచేయుటకు ఎగుమతి చేయాలా?
+ హెచ్చరిక, ఈ నిల్వకు మీ సిగ్నల్ సందేశాలను సాదా విషయాలు ఎగుమతి చేస్తుంది.
+ రద్దు
+ ఎగుమతి అవుతోంది
+ సహజమైన వాక్యాలు నిల్వచేయుటకు ఎగుమతి చేయబడుతున్నవి...
+ లోపం, నిలవచేసే చోటుకు వ్రాయలేరు.
+ నిల్వ చేసే చోటుకు వ్రాసేటపుడు లోపం.
+ ఎగుమతి విజయవంతమైనది.
+ సాధారణ అక్షరాల బ్యాకప్ ఎగుమతి
+ సాదా బ్యాకప్ \'ఎస్ఎంఎస్ బ్యాకప్ మరియు amp అనుకూలంగా ఎగుమతి; నిల్వలో పునరుద్ధరించు \'
+
+ దిగుమతి
+ ఎగుమతి
+
+ దిగుమతి వ్యవస్థ ఎస్సెమ్మెస్ డేటాబేస్?
+ ఇది పరికరం యొక్క అప్రమేయ సందెశ దత్తాంశస్థానం లొని సందేశాలను సిగ్నల్ లోకి ఎగుమతి చేయును. మీరు ఇదివరకే పరికరం యొక్క సందేశ దత్తాంశస్థానన్ని ఎగుమతి చెసి ఉన్న యెడల మరలా ఎగుమతి చెయుట నకిలీ సందెశాలను స్రుష్టించును.
+ దిగుమతి
+ రద్దు
+ సాదా బ్యాకప్ దిగుమతి చేయాలనుకుంటున్నారా?
+ ఇది సందేశాలను సాదా బ్యాకప్ నుండి దిగుమతి చేస్తుంది.మీరు గతంలో ఈ బ్యాకప్ దిగుమతి చేసి ఉంటే,మళ్లీ దిగుమతి నకిలీ సందేశాలను ఫలితమౌతుంది.
+ దిగుమతి అవుతోంది
+ సాదా బ్యాకప్ దిగుమతి అవుతోంది ...
+ సాదా బ్యాకప్ దొరకలేదు!
+ బ్యాకప్ దిగుమతి లోపం!
+ దిగుమతి పూర్తయింది!
+ SMS సందేశాలను దిగుమతి చెయ్యడానికి సిగ్నల్కు SMS అనుమతి అవసరం, కానీ ఇది శాశ్వతంగా తిరస్కరించబడింది. దయచేసి దయచేసి అనువర్తన సెట్టింగ్లకు కొనసాగించండి, \"అనుమతులు\" ఎంచుకోండి మరియు \"SMS\" ను ప్రారంభించండి.
+ SMS సందేశాలను దిగుమతి చేసుకోవడానికి సిగ్నల్కు SMS అనుమతి అవసరం
+ బాహ్య నిల్వ నుండి చదవడానికి సిగ్నల్కు నిల్వ అనుమతి అవసరం, కానీ ఇది శాశ్వతంగా తిరస్కరించబడింది. దయచేసి అనువర్తనం సెట్టింగ్లకు కొనసాగించండి, \"అనుమతులు\" ఎంచుకోండి, ఆపై \"నిల్వ\" ను ప్రారంభించండి.
+ బాహ్య నిల్వ నుండి చదవడానికి సిగ్నల్కు నిల్వ అనుమతి అవసరం.
+ బాహ్య నిల్వకు రాయడానికి సిగ్నల్కు నిల్వ అనుమతి అవసరం, కానీ ఇది శాశ్వతంగా తిరస్కరించబడింది. దయచేసి అనువర్తనం సెట్టింగ్లకు కొనసాగించండి, \"అనుమతులు\" ఎంచుకోండి, ఆపై \"నిల్వ\" ను ప్రారంభించండి.
+ బాహ్య నిల్వకు వ్రాయడానికి సిగ్నల్ కి నిల్వ అనుమతి అవసరం.
+ ఎస్సెమ్మెస్ దత్తాంశమూల వ్యవస్థ దిగుమతి
+ డిఫాల్ట్ వ్యవస్థ మెసెంజర్ అనువర్తనం నుండి దత్తాంశమూల దిగుమతి
+ సాధారణ అక్షరాల ప్రత్యామ్నాయ దిగుమతి
+ స్పష్టమైన వాఖ్యం యొక్క ప్రత్యామ్నాయ ప్రణాళిక పంక్తి ని దిగుమతి చేయండి. \'SMS బ్యాకప్ & పునరుద్ధరణతో అనుకూలమైనది.\'
+
diff --git a/app/src/main/res/values-th/strings1.xml b/app/src/main/res/values-th/strings1.xml
new file mode 100644
index 00000000000..7472966ea78
--- /dev/null
+++ b/app/src/main/res/values-th/strings1.xml
@@ -0,0 +1,8 @@
+
+
+ นำเข้า / ส่งออก
+ นำเข้าฐานข้อมูล SMS ของระบบ
+ นำเข้าฐานข้อมูลจากแอปรับส่งข้อความเริ่มต้นของระบบ
+ นำเข้าข้อมูลสำรองที่ไม่ได้เข้ารหัสลับ
+ นำเข้าแฟ้มข้อมูลสำรองที่ไม่ได้เข้ารหัสลับ ใช้ร่วมกันได้กับ \'การสำรองข้อมูลและกู้คืน SMS\'
+
diff --git a/app/src/main/res/values-tr/strings1.xml b/app/src/main/res/values-tr/strings1.xml
new file mode 100644
index 00000000000..a7e467bbf60
--- /dev/null
+++ b/app/src/main/res/values-tr/strings1.xml
@@ -0,0 +1,27 @@
+
+
+
+ İçe/Dışa Aktar
+
+ Şifrelenmiş yedek yeniden yüklensin mi?
+
+Şifreli bir yedeği geri yüklemek mevcut anahtarların, tercihlerin ve mesajların yerini tamamen alacak. Bu işlem uygulamanın bütün verilerini silip, yedekteki verileri geri yükleyecektir.
+ Geri yükle
+ Geri yükleniyor
+ Şifrelenmiş yedek geri yükleniyor...
+ Şifrelenmiş yedek bulunamadı!
+ Geri yükleme tamamlandı!
+
+ SD karta kaydet?
+ Bu\nşifrelenmiş anahtar, ayar ve mesajları SD karta kaydecek.
+ Şifrelenmiş anahtar, ayar \nve mesajlar kaydediliyor...
+
+ Şifrelenmiş yedeği dışa aktar
+ Şifrelenmiş bir yedeği\n SD karta kaydet.\n
+ Şifrelenmiş yedeğini geri yükleyin
+ Daha önce dışa aktarılmış şifreli Signal yedeğini geri yükleyin
+ Sistem SMS veritabanını içe aktar
+ Varsayılan sistem ileti uygulamasının veritabanını içe aktar
+ Şifrelenmemiş metin yedeğini içe aktar
+ SMS yedeği ile uyumlu bir metin yedeğini İçe aktar & Geri yükle
+
diff --git a/app/src/main/res/values-uk/strings1.xml b/app/src/main/res/values-uk/strings1.xml
new file mode 100644
index 00000000000..726faed1785
--- /dev/null
+++ b/app/src/main/res/values-uk/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Імпорт / експорт
+
+ Відновити зашифровану резервну копію?
+
+Відновлення зашифрованої резервної копії повністю замінить поточні ключі, налаштування
+та повідомлення. Ви не втратите ані жодної інформації, що наразі є у Signal, ані у резервній копії.
+
+ Відновити
+ Відновлення
+ Відновлення зашифрованої резервної копії
+ Зашифрована резервна копія не знайдена!
+ Відновлення завершено!
+
+ Експортувати на SD-карту?
+ Це експортує ваші шифровані ключі, налаштування та повідомлення на SD карту.
+ Експорт шифрованих ключів, налаштувань та повідомлень\u2026
+
+ Експорт Зашифрованої Резервної копії
+ Експортувати на SD-карту\nзашифровану резервну копію.
+ Відновити зашифровану резервну копію
+ Відновити раніше експортовану зашифровану резервну копію Signal
+ Імпортувати системну базу SMS
+ Імпортувати дані з типової програми обміну повідомленнями
+ Імпорт незашифрованої резервної копії
+ Імпортувати незашифровану резервну копію, сумісну з програмою \'SMSBackup & Restore\'.
+
diff --git a/app/src/main/res/values-vi/strings1.xml b/app/src/main/res/values-vi/strings1.xml
new file mode 100644
index 00000000000..a4eb213c480
--- /dev/null
+++ b/app/src/main/res/values-vi/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ Xuất / nhập
+
+ Phục hồi phần sao lưu mã hóa?
+
+Khôi phục sao lưu đã mã khoá sẽ thay thế hoàn toàn các khoá, tinh chỉnh và tin nhắn hiện hữu của bạn. Bạn cũng sẽ mất tất cả thông tin trong bản cài đặt Signal hiện tại ngoại trừ
+trong bản sao lưu.
+
+ Phục hồi
+ Đang phục hồi
+ Phục hồi sao lưu có mã hóa...
+ Không tìm thấy sao lưu có mã hóa!
+ Phục hồi hoàn tất!
+
+ Xuất ra thẻ nhớ SD?
+ Thao tác này sẽ xuất các chìa khóa mã hóa, thiết đặt, và tin nhắn của bạn ra thẻ nhớ SD.
+ Đang xuất ra các chìa khóa mã hóa, thiết đặt, và tin nhắn...
+
+ Xuất Sao Lưu Có Mã Hóa
+ Xuất sao lưu có mã hóa ra thẻ nhớ SD
+ Phục hồi sao lưu đã mã khóa
+ Phục hồi sao lưu Signal đã mã khóa từng xuất ra trước đó
+ Nhập cơ sở dữ liệu SMS của hệ thống
+ Nhập cơ sở dữ liệu từ ứng dụng tin nhắn mặc định của hệ thống
+ Nhập sao lưu văn bản thường
+ Nhập tập tin sao lưu văn bản không mã hoá. Tương thích với \'Sao lưu & Phục hồi SMS.\'
+
diff --git a/app/src/main/res/values-zh-rCN/strings1.xml b/app/src/main/res/values-zh-rCN/strings1.xml
new file mode 100644
index 00000000000..108e9ab916f
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/strings1.xml
@@ -0,0 +1,29 @@
+
+
+
+ 导入/导出
+
+ 恢复加密备份?
+
+恢复加密的备份将会完全替换您已经存在的密钥、首选项和
+信息。您将会失去当前暗号上的所有信息,除非您已经备份。
+ 还原
+ 正在还原
+ 正在还原加密的备份…
+ 未找到加密的备份!
+ 还原完成!
+
+ 导出到SD卡?
+ 这会将您的密钥、设置和信息加密后导出到SD卡。
+ 正在导出加密后的密钥、设置和信息…
+
+
+ 导出加密的备份
+ 将加密的备份导出到SD卡。
+ 恢复加密备份
+ 还原之前导出的加密暗号备份。
+ 导入系统短信数据库
+ 从系统默认短信应用导入数据库
+ 导入明文备份
+ 导入明文备份文件。兼容“SMS Backup & Restore”备份。
+
diff --git a/app/src/main/res/values-zh-rTW/strings1.xml b/app/src/main/res/values-zh-rTW/strings1.xml
new file mode 100644
index 00000000000..4b085235072
--- /dev/null
+++ b/app/src/main/res/values-zh-rTW/strings1.xml
@@ -0,0 +1,9 @@
+
+
+
+ 匯入/匯出
+ 匯入裝置上的手機簡訊
+ 從系統預設訊息程序匯入數據庫
+ 匯入未加密的資料備份
+ 匯入一個純文字格式備份。相容於“SMS Backup & Restore.”
+
diff --git a/app/src/main/res/values/arrays1.xml b/app/src/main/res/values/arrays1.xml
new file mode 100644
index 00000000000..6fc0da0adbb
--- /dev/null
+++ b/app/src/main/res/values/arrays1.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ - @string/preferences__map_normal
+ - @string/preferences__map_hybrid
+ - @string/preferences__map_satellite
+ - @string/preferences__map_terrain
+ - @string/preferences__map_none
+
+
+
+
+ - normal
+ - hybrid
+ - satellite
+ - terrain
+ - none
+
+
+
+ - @string/preferences__group_anyone
+ - @string/preferences__group_non_blocked
+ - @string/preferences__group_only_contats
+ - @string/preferences__group_only_systemcontats
+ - @string/preferences__group_nobody
+
+
+
+ - anyone
+ - nonblocked
+ - onlycontacts
+ - onlysystemcontacts
+ - nobody
+
+
diff --git a/app/src/main/res/values/firebase_messaging.xml b/app/src/main/res/values/firebase_messaging.xml
index 6cd44588a46..6e825140c0e 100644
--- a/app/src/main/res/values/firebase_messaging.xml
+++ b/app/src/main/res/values/firebase_messaging.xml
@@ -1,10 +1,10 @@
- 1:312334754206:android:a9297b152879f266
+ 1:544173211933:android:94704405e794a9a55021f4
312334754206
312334754206-dg1p1mtekis8ivja3ica50vonmrlunh4.apps.googleusercontent.com
https://api-project-312334754206.firebaseio.com
- AIzaSyDrfzNAPBPzX6key51hqo3p5LZXF5Y-yxU
- AIzaSyDrfzNAPBPzX6key51hqo3p5LZXF5Y-yxU
- api-project-312334754206
+ AIzaSyAkCi29YyBuU7jpBDHah4XXRUPTmeSqjRY
+ AIzaSyAkCi29YyBuU7jpBDHah4XXRUPTmeSqjRY
+ signal-jw
\ No newline at end of file
diff --git a/app/src/main/res/values/strings1.xml b/app/src/main/res/values/strings1.xml
new file mode 100644
index 00000000000..69b31ec9ffd
--- /dev/null
+++ b/app/src/main/res/values/strings1.xml
@@ -0,0 +1,140 @@
+
+
+
+ Import / export
+
+ OLED Dark
+ Green Light
+ Blue Light
+ Location
+ Map type
+ Normal
+ Hybrid
+ Satellite
+ Terrain
+ None
+ Anyone
+ All non-blocked persons
+ Only contacts
+ Only non-blocked system contacts
+ Nobody
+
+ Use a passphrase
+ Use a passphrase to protect Signal instead of the Android screen lock or fingerprint
+ Using the system screenlock requires Android 5.0 (Lollipop) or higher.
+ Android version too low
+
+ Push Notifications via FCM
+ Receive push notifications via Google Play Services. When switched off a websocket will be used, which might cause more battery usage
+ Google Play Services not found
+ Signal needs to restart for the changes to take effect
+
+ Control message deletion
+ Keep view-once media
+ When checked incoming view-once media will be treated as normal view-always media and will not be deleted after view
+ Ignore remote delete
+ When checked, this option prevents the remote deletion of messages by the sender
+ Delete media only
+ Delete only media, not the message itself, when media is deleted in the All media screen
+
+ Backup location
+ Backup location (tap to change)
+ Backup chats to removable storage (if available)
+
+ Store encrypted backups in zipfile
+ The zipfile will be encrypted with the same password as the regular backups, or no password if regular backup is not set
+ Store plaintext backups in zipfile
+ The zipfile will be encrypted with the same password as the regular backups, or no password if regular backup is not set
+
+ Import
+ Export
+
+ Import system SMS database?
+ This will import
+ messages from the system\'s default SMS database to Signal. If you\'ve previously
+ imported the system\'s SMS database, importing again will result in duplicated messages.
+
+ Import
+ Cancel
+ Import plaintext backup?
+ This will import
+ messages from a plaintext backup. If you\'ve previously imported this backup,
+ importing again will result in duplicated messages.
+
+ Importing
+ Importing plaintext backup...
+ No plaintext backup found!
+ Error importing backup!
+ Import complete!
+ Import system SMS database
+ Import the database from the default system messenger app
+ Import plaintext backup
+ Import a plaintext backup file. Compatible with \'SMS Backup & Restore.\'
+
+ Signal needs the SMS permission in order to import SMS messages, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"SMS\".
+ Signal needs the SMS permission in order to import SMS messages
+ Signal needs the Storage permission in order to read from external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", then enable \"Storage\".
+ Signal needs the Storage permission in order to read from external storage.
+ Signal needs the Storage permission in order to write to external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", then enable \"Storage\".
+ Signal needs the Storage permission in order to write to external storage.
+ Signal needs the All Files Access permission in order to make this kind of backups.
+
+ Restore encrypted backup?
+
+ Restoring an encrypted backup will completely replace your existing keys, preferences, and
+ messages. You will lose any information that\'s in your current Signal install but not
+ in the backup.
+
+ Restore
+ Restoring
+ Restoring encrypted backup...
+ No encrypted backup found!
+ Restore complete!
+ OK
+
+ No WhatsApp Backup found! Make sure msgstore.db is in the root directory of the phone\'s internal storage.
+ Import WhatsApp backup?
+ This will import
+ messages from an unencrypted WhatsApp msgstore.db backup file located in the root of the internal sd card. If you\'ve previously imported this backup, importing again will result in duplicated messages.
+
+ Importing whatsapp backup...
+ Import WhatsApp backup
+ Import a WhatsApp db file. Place msgstore.db in the root directory
+ Import media messages
+ Import group chats if Signal group with the same name exists
+ Avoid duplicate messages
+
+ Export To SD Card?
+ This will export your encrypted keys, settings, and messages to the SD card.
+ Exporting encrypted keys, settings, and messages...
+ Export
+ Export plaintext to storage?
+ Warning, this will export the plaintext contents of your Signal messages to storage.
+ Cancel
+ Exporting
+ Exporting plaintext to storage...
+ Error, unable to write to storage.
+ Error while writing to storage.
+ Export successful.
+
+ Restore encrypted backup
+ Restore a previously exported encrypted Signal backup
+ Export encrypted backup
+ Export an encrypted backup to the SD card.
+ Export plaintext backup
+ Export a plaintext backup compatible with \'SMS Backup & Restore\' to storage
+
+ Restore encrypted backup
+
+ Group control
+ Who can add you to groups
+
+ Reset SMS export status
+ Reset SMS export status
+ You can reset the SMS export status so you can export again
+ Reset export status for %d messages
+ Resetting the export status...
+ Do not change the export status
+ You can export SMS messages from Signal again from Settings...
+ Keep the export status as it is
+
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index cbb733a340a..d0dc6baa213 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -9096,5 +9096,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+