Support for full backup/restore to sdcard
This commit is contained in:
parent
9f6b761d98
commit
24e573e537
41 changed files with 5884 additions and 269 deletions
43
protobuf/Backups.proto
Normal file
43
protobuf/Backups.proto
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (C) 2018 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.thoughtcrime.securesms.backup";
|
||||
option java_outer_classname = "BackupProtos";
|
||||
|
||||
message SqlStatement {
|
||||
optional string statement = 1;
|
||||
}
|
||||
|
||||
message SharedPreference {
|
||||
optional string file = 1;
|
||||
optional string key = 2;
|
||||
optional string value = 3;
|
||||
}
|
||||
|
||||
message Attachment {
|
||||
optional uint64 rowId = 1;
|
||||
optional uint64 attachmentId = 2;
|
||||
optional uint32 length = 3;
|
||||
}
|
||||
|
||||
message DatabaseVersion {
|
||||
optional uint32 version = 1;
|
||||
}
|
||||
|
||||
message Header {
|
||||
optional bytes iv = 1;
|
||||
}
|
||||
|
||||
message BackupFrame {
|
||||
optional Header header = 1;
|
||||
optional SqlStatement statement = 2;
|
||||
optional SharedPreference preference = 3;
|
||||
optional Attachment attachment = 4;
|
||||
optional DatabaseVersion version = 5;
|
||||
optional bool end = 6;
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
all:
|
||||
protoc --java_out=../src/ WebRtcData.proto
|
||||
protoc --java_out=../src/ WebRtcData.proto Backups.proto
|
||||
|
|
BIN
res/drawable-hdpi/ic_restore_white_24dp.png
Normal file
BIN
res/drawable-hdpi/ic_restore_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 509 B |
BIN
res/drawable-mdpi/ic_restore_white_24dp.png
Normal file
BIN
res/drawable-mdpi/ic_restore_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
BIN
res/drawable-xhdpi/ic_restore_white_24dp.png
Normal file
BIN
res/drawable-xhdpi/ic_restore_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 643 B |
BIN
res/drawable-xxhdpi/ic_restore_white_24dp.png
Normal file
BIN
res/drawable-xxhdpi/ic_restore_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 945 B |
BIN
res/drawable-xxxhdpi/ic_restore_white_24dp.png
Normal file
BIN
res/drawable-xxxhdpi/ic_restore_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
95
res/layout/backup_enable_dialog.xml
Normal file
95
res/layout/backup_enable_dialog.xml
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingLeft="23dp"
|
||||
android:paddingRight="23dp">
|
||||
|
||||
<TextView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:text="@string/backup_enable_dialog__backups_will_be_saved_to_external_storage_and_encrypted_with_the_passphrase_below_you_must_have_this_passphrase_in_order_to_restore_a_backup"/>
|
||||
|
||||
<TableLayout android:id="@+id/number_table"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TableRow android:gravity="center_horizontal"
|
||||
android:clickable="false"
|
||||
android:focusable="false">
|
||||
|
||||
<TextView android:id="@+id/code_first"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="22934"/>
|
||||
|
||||
<TextView android:id="@+id/code_second"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="56944"
|
||||
android:layout_marginStart="20dp"/>
|
||||
|
||||
<TextView android:id="@+id/code_third"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="42738"
|
||||
android:layout_marginStart="20dp"/>
|
||||
</TableRow>
|
||||
|
||||
<TableRow android:gravity="center_horizontal">
|
||||
<TextView android:id="@+id/code_fourth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="34431"/>
|
||||
|
||||
<TextView android:id="@+id/code_fifth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="24922"
|
||||
android:layout_marginStart="20dp"/>
|
||||
|
||||
<TextView android:id="@+id/code_sixth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
style="@style/BackupPassphrase"
|
||||
tools:text="58594"
|
||||
android:layout_marginStart="20dp"/>
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
|
||||
<LinearLayout android:layout_marginTop="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<CheckBox android:id="@+id/confirmation_check"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginEnd="10dp"/>
|
||||
|
||||
<TextView android:id="@+id/confirmation_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:text="@string/backup_enable_dialog__i_have_written_down_this_passphrase"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
20
res/layout/enter_backup_passphrase_dialog.xml
Normal file
20
res/layout/enter_backup_passphrase_dialog.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/restore_passphrase_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/enter_backup_passphrase_dialog__backup_passphrase"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textVisiblePassword" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</FrameLayout>
|
|
@ -109,68 +109,5 @@
|
|||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/preference_divider"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/ImportExportActivity_export"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="@color/signal_primary_dark"/>
|
||||
|
||||
<LinearLayout android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout android:id="@+id/export_plaintext_backup"
|
||||
android:clickable="true"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<ImageView android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:src="@drawable/ic_content_copy_white_24dp"
|
||||
android:tint="?attr/pref_icon_tint"/>
|
||||
|
||||
<LinearLayout android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<TextView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
style="@style/Registration.Description"
|
||||
android:text="@string/export_fragment__export_plaintext_backup"/>
|
||||
|
||||
<TextView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/export_fragment__export_a_plaintext_backup_compatible_with"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
23
res/layout/preference_widget_progress.xml
Normal file
23
res/layout/preference_widget_progress.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="16dp"
|
||||
android:gravity="bottom">
|
||||
|
||||
<ProgressBar android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
<TextView android:id="@+id/progress_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
tools:text="1345 messages so far"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -55,6 +55,64 @@
|
|||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="-32dp"/>
|
||||
|
||||
<LinearLayout android:id="@+id/restore_container"
|
||||
android:padding="16dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_below="@id/header"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView android:id="@+id/backup_created_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Backup created: 1 min ago"/>
|
||||
|
||||
<TextView android:id="@+id/backup_size_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
tools:text="Backup size: 899 KB"/>
|
||||
|
||||
<TextView android:id="@+id/backup_progress_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
tools:text="100 messages so far..."/>
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/restore_button"
|
||||
app:cpb_textIdle="@string/registration_activity__restore_backup"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/textsecure_primary"
|
||||
app:cpb_cornerRadius="50dp"
|
||||
android:background="@color/signal_primary"
|
||||
android:textColor="@color/white"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
|
||||
<TextView android:id="@+id/skip_restore_button"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="13dp"
|
||||
android:textColor="@color/gray50"
|
||||
android:paddingLeft="30dp"
|
||||
android:paddingRight="30dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/registration_activity__skip"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout android:id="@+id/registration_container"
|
||||
android:padding="16dp"
|
||||
android:paddingBottom="0dp"
|
||||
|
@ -104,7 +162,7 @@
|
|||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/registerButton"
|
||||
app:cpb_textIdle="Register"
|
||||
app:cpb_textIdle="@string/registration_activity__register"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/textsecure_primary"
|
||||
|
@ -168,7 +226,7 @@
|
|||
android:layout_below="@id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:visibility="visible">
|
||||
tools:visibility="invisible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.VerificationCodeView
|
||||
android:id="@+id/code"
|
||||
|
|
|
@ -1029,7 +1029,7 @@
|
|||
<string name="AndroidManifest_remove_photo">Remove photo</string>
|
||||
|
||||
<!-- arrays.xml -->
|
||||
<string name="arrays__import_export">Import / export</string>
|
||||
<string name="arrays__import_export">Import</string>
|
||||
<string name="arrays__use_default">Use default</string>
|
||||
<string name="arrays__use_custom">Use custom</string>
|
||||
|
||||
|
@ -1326,6 +1326,40 @@
|
|||
<string name="PushDecryptJob_unlock_to_view_pending_messages">Unlock to view pending messages</string>
|
||||
<string name="ExperienceUpgradeActivity_unlock_to_complete_update">Unlock to complete update</string>
|
||||
<string name="ExperienceUpgradeActivity_please_unlock_signal_to_complete_update">Please unlock Signal to complete update</string>
|
||||
<string name="enter_backup_passphrase_dialog__backup_passphrase">Backup passphrase</string>
|
||||
<string name="backup_enable_dialog__backups_will_be_saved_to_external_storage_and_encrypted_with_the_passphrase_below_you_must_have_this_passphrase_in_order_to_restore_a_backup">Backups will be saved to external storage and encrypted with the passphrase below. You must have this passphrase in order to restore a backup.</string>
|
||||
<string name="backup_enable_dialog__i_have_written_down_this_passphrase">I have written down this passphrase. Without it, I will be unable to restore a backup.</string>
|
||||
<string name="registration_activity__restore_backup">Restore backup</string>
|
||||
<string name="registration_activity__skip">Skip</string>
|
||||
<string name="registration_activity__register">Register</string>
|
||||
<string name="preferences_chats__chat_backups">Chat backups</string>
|
||||
<string name="preferences_chats__backup_chats_to_external_storage">Backup chats to external storage</string>
|
||||
<string name="preferences_chats__create_backup">Create backup</string>
|
||||
<string name="RegistrationActivity_enter_backup_passphrase">Enter backup passphrase</string>
|
||||
<string name="RegistrationActivity_restore">Restore</string>
|
||||
<string name="RegistrationActivity_incorrect_backup_passphrase">Incorrect backup password</string>
|
||||
<string name="RegistrationActivity_checking">Checking...</string>
|
||||
<string name="RegistrationActivity_d_messages_so_far">%d messages so far...</string>
|
||||
<string name="RegistrationActivity_restore_from_backup">Restore from backup?</string>
|
||||
<string name="RegistrationActivity_restore_your_messages_and_media_from_a_local_backup">Restore your messages and media from a local backup. If you don\'t restore now, you won\'t be able to restore later.</string>
|
||||
<string name="RegistrationActivity_backup_size_s">Backup size: %s</string>
|
||||
<string name="RegistrationActivity_backup_timestamp_s">Backup timestamp: %s</string>
|
||||
<string name="BackupDialog_enable_local_backups">Enable local backups?</string>
|
||||
<string name="BackupDialog_enable_backups">Enable backups</string>
|
||||
<string name="BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box">Please acknowledge your understanding by marking the confirmation check box.</string>
|
||||
<string name="BackupDialog_delete_backups">Delete backups?</string>
|
||||
<string name="BackupDialog_disable_and_delete_all_local_backups">Disable and delete all local backups?</string>
|
||||
<string name="BackupDialog_delete_backups_statement">Delete backups</string>
|
||||
<string name="BackupDialog_copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups">Signal requires external storage permission in order to create backups, but it has been permanently denied. Please continue to app settings, select \"Permissions\" and enable \"External Storage\".</string>
|
||||
<string name="ChatsPreferenceFragment_last_backup_s">Last backup: %s</string>
|
||||
<string name="ChatsPreferenceFragment_in_progress">In progress</string>
|
||||
<string name="ProgressPreference_d_messages_so_far">%d messages so far</string>
|
||||
<string name="RegistrationActivity_verify_s">Verify %s</string>
|
||||
<string name="RegistrationActivity_please_enter_the_verification_code_sent_to_s">Please enter the verification code sent to %s.</string>
|
||||
<string name="RegistrationActivity_wrong_number">Wrong number?</string>
|
||||
<string name="BackupUtil_never">Never</string>
|
||||
<string name="BackupUtil_unknown">Unknown</string>
|
||||
|
||||
|
||||
<!-- EOF -->
|
||||
|
|
|
@ -207,6 +207,15 @@
|
|||
<item name="android:focusable">false</item>
|
||||
</style>
|
||||
|
||||
<style name="BackupPassphrase">
|
||||
<item name="android:fontFamily">monospace</item>
|
||||
<item name="android:typeface">monospace</item>
|
||||
<item name="android:textSize">15sp</item>
|
||||
<item name="android:clickable">false</item>
|
||||
<item name="android:focusable">false</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="PreferenceThemeOverlay.Fix" parent="PreferenceThemeOverlay.v14.Material">
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<PreferenceCategory android:key="media_download" android:title="@string/preferences_chats__media_auto_download">
|
||||
<MultiSelectListPreference
|
||||
android:title="@string/preferences_chats__when_using_mobile_data"
|
||||
|
@ -77,4 +78,23 @@
|
|||
android:summary="@string/preferences__scan_through_all_conversations_and_enforce_conversation_length_limits"
|
||||
android:dependency="pref_trim_threads" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:key="backup_category" android:title="Backups">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_backup_enabled"
|
||||
android:title="@string/preferences_chats__chat_backups"
|
||||
android:summary="@string/preferences_chats__backup_chats_to_external_storage" />
|
||||
|
||||
<org.thoughtcrime.securesms.preferences.widgets.ProgressPreference
|
||||
android:key="pref_backup_create"
|
||||
android:title="@string/preferences_chats__create_backup"
|
||||
android:persistent="false"
|
||||
android:dependency="pref_backup_enabled"
|
||||
tools:summary="Last backup: 3 days ago"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequiremen
|
|||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -156,6 +157,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
LocalBackupListener.schedule(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -35,12 +34,9 @@ public class ExpirationDialog extends AlertDialog {
|
|||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
|
@ -49,8 +45,8 @@ public class ExpirationDialog extends AlertDialog {
|
|||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
||||
final NumberPickerView numberPickerView = (NumberPickerView)view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = (TextView)view.findViewById(R.id.expiration_details);
|
||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
||||
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
||||
|
||||
|
@ -69,14 +65,11 @@ public class ExpirationDialog extends AlertDialog {
|
|||
numberPickerView.setMinValue(0);
|
||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
||||
|
||||
NumberPickerView.OnValueChangeListener listener = new NumberPickerView.OnValueChangeListener() {
|
||||
@Override
|
||||
public void onValueChange(NumberPickerView picker, int oldVal, int newVal) {
|
||||
if (newVal == 0) {
|
||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
||||
} else {
|
||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
||||
}
|
||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
||||
if (newVal == 0) {
|
||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
||||
} else {
|
||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import android.view.ViewGroup;
|
|||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
|
||||
import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
|
@ -42,15 +41,13 @@ public class ImportExportFragment extends Fragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
View layout = inflater.inflate(R.layout.import_export_fragment, container, false);
|
||||
View importSmsView = layout.findViewById(R.id.import_sms );
|
||||
View importPlaintextView = layout.findViewById(R.id.import_plaintext_backup);
|
||||
View exportPlaintextView = layout.findViewById(R.id.export_plaintext_backup);
|
||||
|
||||
importSmsView.setOnClickListener(v -> handleImportSms());
|
||||
importPlaintextView.setOnClickListener(v -> handleImportPlaintextBackup());
|
||||
exportPlaintextView.setOnClickListener(v -> handleExportPlaintextBackup());
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
@ -119,26 +116,6 @@ public class ImportExportFragment extends Fragment {
|
|||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void handleExportPlaintextBackup() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
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) -> {
|
||||
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();
|
||||
});
|
||||
builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class ImportPlaintextBackupTask extends AsyncTask<Void, Void, Integer> {
|
||||
|
||||
|
@ -192,62 +169,4 @@ public class ImportExportFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class ExportPlaintextTask extends AsyncTask<Void, Void, Integer> {
|
||||
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("ExportFragment", e);
|
||||
return NO_SD_CARD;
|
||||
} catch (IOException e) {
|
||||
Log.w("ExportFragment", 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -26,10 +26,12 @@ import android.text.style.ClickableSpan;
|
|||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
@ -42,22 +44,33 @@ import com.google.i18n.phonenumbers.AsYouTypeFormatter;
|
|||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter;
|
||||
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus;
|
||||
|
@ -75,6 +88,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The register account activity. Prompts ths user for their registration information
|
||||
|
@ -89,6 +103,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
private static final int SCENE_TRANSITION_DURATION = 250;
|
||||
public static final String CHALLENGE_EVENT = "org.thoughtcrime.securesms.CHALLENGE_EVENT";
|
||||
public static final String CHALLENGE_EXTRA = "CAAChallenge";
|
||||
public static final String RE_REGISTRATION_EXTRA = "re_registration";
|
||||
|
||||
private static final String TAG = RegistrationActivity.class.getSimpleName();
|
||||
|
||||
|
@ -106,6 +121,12 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
private View verificationContainer;
|
||||
private FloatingActionButton fab;
|
||||
|
||||
private View restoreContainer;
|
||||
private TextView restoreBackupTime;
|
||||
private TextView restoreBackupSize;
|
||||
private TextView restoreBackupProgress;
|
||||
private CircularProgressButton restoreButton;
|
||||
|
||||
private CallMeCountDownView callMeCountDownView;
|
||||
private VerificationPinKeyboard keyboard;
|
||||
private VerificationCodeView verificationCodeView;
|
||||
|
@ -113,6 +134,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
private ChallengeReceiver challengeReceiver;
|
||||
private SignalServiceAccountManager accountManager;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
@ -130,6 +152,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
super.onDestroy();
|
||||
shutdownChallengeListener();
|
||||
markAsVerifying(false);
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -147,8 +170,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
}
|
||||
|
||||
private void initializeResources() {
|
||||
TextView skipButton = findViewById(R.id.skip_button);
|
||||
View informationToggle = findViewById(R.id.information_link_container);
|
||||
TextView skipButton = findViewById(R.id.skip_button);
|
||||
TextView restoreSkipButton = findViewById(R.id.skip_restore_button);
|
||||
View informationToggle = findViewById(R.id.information_link_container);
|
||||
|
||||
this.countrySpinner = findViewById(R.id.country_spinner);
|
||||
this.countryCode = findViewById(R.id.country_code);
|
||||
|
@ -165,6 +189,13 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
this.verificationCodeView = findViewById(R.id.code);
|
||||
this.keyboard = findViewById(R.id.keyboard);
|
||||
this.callMeCountDownView = findViewById(R.id.call_me_count_down);
|
||||
|
||||
this.restoreContainer = findViewById(R.id.restore_container);
|
||||
this.restoreBackupSize = findViewById(R.id.backup_size_text);
|
||||
this.restoreBackupTime = findViewById(R.id.backup_created_text);
|
||||
this.restoreBackupProgress = findViewById(R.id.backup_progress_text);
|
||||
this.restoreButton = findViewById(R.id.restore_button);
|
||||
|
||||
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
|
||||
|
||||
this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
|
||||
|
@ -174,7 +205,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
skipButton.setOnClickListener(v -> handleCancel());
|
||||
informationToggle.setOnClickListener(new InformationToggleListener());
|
||||
|
||||
if (getIntent().getBooleanExtra("cancel_button", false)) {
|
||||
restoreSkipButton.setOnClickListener(v -> displayInitialView(true));
|
||||
|
||||
if (getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false)) {
|
||||
skipButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
skipButton.setVisibility(View.INVISIBLE);
|
||||
|
@ -186,6 +219,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
});
|
||||
|
||||
this.verificationCodeView.setOnCompleteListener(this);
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
|
@ -247,10 +281,36 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
if (permissions.contains(Manifest.permission.READ_PHONE_STATE)) {
|
||||
initializeNumber();
|
||||
}
|
||||
|
||||
if (permissions.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
initializeBackupDetection();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeBackupDetection() {
|
||||
if (getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false)) return;
|
||||
|
||||
new AsyncTask<Void, Void, BackupUtil.BackupInfo>() {
|
||||
@Override
|
||||
protected @Nullable BackupUtil.BackupInfo doInBackground(Void... voids) {
|
||||
try {
|
||||
return BackupUtil.getLatestBackup();
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) {
|
||||
if (backup != null) displayRestoreView(backup);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void setCountryDisplay(String value) {
|
||||
this.countrySpinnerAdapter.clear();
|
||||
this.countrySpinnerAdapter.add(value);
|
||||
|
@ -269,6 +329,60 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
number.getText().toString());
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleRestore(BackupUtil.BackupInfo backup) {
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
|
||||
.setView(view)
|
||||
.setPositiveButton(getString(R.string.RegistrationActivity_restore), (dialog, which) -> {
|
||||
restoreButton.setIndeterminateProgressMode(true);
|
||||
restoreButton.setProgress(50);
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
Context context = RegistrationActivity.this;
|
||||
String passphrase = prompt.getText().toString();
|
||||
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
|
||||
|
||||
FullBackupImporter.importFile(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database, backup.getFile(), passphrase);
|
||||
|
||||
DatabaseFactory.upgradeRestored(context, database);
|
||||
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
TextSecurePreferences.setBackupPassphrase(context, passphrase);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@NonNull Boolean result) {
|
||||
restoreButton.setIndeterminateProgressMode(false);
|
||||
restoreButton.setProgress(0);
|
||||
restoreBackupProgress.setText("");
|
||||
|
||||
if (result) {
|
||||
displayInitialView(true);
|
||||
} else {
|
||||
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleRegister() {
|
||||
if (TextUtils.isEmpty(countryCode.getText())) {
|
||||
Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
|
||||
|
@ -491,11 +605,11 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
}
|
||||
}
|
||||
|
||||
private void displayInitialView(@NonNull String e164number) {
|
||||
private void displayRestoreView(@NonNull BackupUtil.BackupInfo backup) {
|
||||
title.animate().translationX(title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
title.setText(R.string.registration_activity__verify_your_number);
|
||||
title.setText(R.string.RegistrationActivity_restore_from_backup);
|
||||
title.clearAnimation();
|
||||
title.setTranslationX(-1 * title.getWidth());
|
||||
title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
|
||||
|
@ -505,21 +619,80 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
subtitle.animate().translationX(subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
subtitle.setText(R.string.registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply);
|
||||
subtitle.setText(R.string.RegistrationActivity_restore_your_messages_and_media_from_a_local_backup);
|
||||
subtitle.clearAnimation();
|
||||
subtitle.setTranslationX(-1 * subtitle.getWidth());
|
||||
subtitle.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
|
||||
}
|
||||
}).start();
|
||||
|
||||
verificationContainer.animate().translationX(verificationContainer.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
registrationContainer.animate().translationX(registrationContainer.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
verificationContainer.clearAnimation();
|
||||
verificationContainer.setVisibility(View.INVISIBLE);
|
||||
verificationContainer.setTranslationX(0);
|
||||
registrationContainer.clearAnimation();
|
||||
registrationContainer.setVisibility(View.INVISIBLE);
|
||||
registrationContainer.setTranslationX(0);
|
||||
|
||||
registrationContainer.setTranslationX(-1 * registrationContainer.getWidth());
|
||||
restoreContainer.setTranslationX(-1 * registrationContainer.getWidth());
|
||||
restoreContainer.setVisibility(View.VISIBLE);
|
||||
restoreButton.setProgress(0);
|
||||
restoreButton.setIndeterminateProgressMode(false);
|
||||
restoreButton.setOnClickListener(v -> handleRestore(backup));
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(RegistrationActivity.this, Locale.US, backup.getTimestamp())));
|
||||
restoreBackupProgress.setText("");
|
||||
restoreContainer.animate().translationX(0).setDuration(SCENE_TRANSITION_DURATION).setListener(null).setInterpolator(new OvershootInterpolator()).start();
|
||||
}
|
||||
}).start();
|
||||
|
||||
fab.animate().rotationBy(375f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fab.clearAnimation();
|
||||
fab.setImageResource(R.drawable.ic_restore_white_24dp);
|
||||
fab.animate().rotationBy(360f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
private void displayInitialView(boolean forwards) {
|
||||
int startDirectionMultiplier = forwards ? -1 : 1;
|
||||
int endDirectionMultiplier = forwards ? 1 : -1;
|
||||
|
||||
title.animate().translationX(startDirectionMultiplier * title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
title.setText(R.string.registration_activity__verify_your_number);
|
||||
title.clearAnimation();
|
||||
title.setTranslationX(endDirectionMultiplier * title.getWidth());
|
||||
title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
|
||||
}
|
||||
}).start();
|
||||
|
||||
subtitle.animate().translationX(startDirectionMultiplier * subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
subtitle.setText(R.string.registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply);
|
||||
subtitle.clearAnimation();
|
||||
subtitle.setTranslationX(endDirectionMultiplier * subtitle.getWidth());
|
||||
subtitle.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
|
||||
}
|
||||
}).start();
|
||||
|
||||
View container;
|
||||
|
||||
if (verificationContainer.getVisibility() == View.VISIBLE) container = verificationContainer;
|
||||
else container = restoreContainer;
|
||||
|
||||
container.animate().translationX(startDirectionMultiplier * container.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
container.clearAnimation();
|
||||
container.setVisibility(View.INVISIBLE);
|
||||
container.setTranslationX(0);
|
||||
|
||||
registrationContainer.setTranslationX(endDirectionMultiplier * registrationContainer.getWidth());
|
||||
registrationContainer.setVisibility(View.VISIBLE);
|
||||
createButton.setProgress(0);
|
||||
createButton.setIndeterminateProgressMode(false);
|
||||
|
@ -527,12 +700,12 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
}
|
||||
}).start();
|
||||
|
||||
fab.animate().rotationBy(360f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
fab.animate().rotationBy(startDirectionMultiplier * 360f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fab.clearAnimation();
|
||||
fab.setImageResource(R.drawable.ic_action_name);
|
||||
fab.animate().rotationBy(375f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
|
||||
fab.animate().rotationBy(startDirectionMultiplier * 375f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
@ -547,7 +720,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
title.animate().translationX(-1 * title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
title.setText(String.format("Verify %s", e164number));
|
||||
title.setText(getString(R.string.RegistrationActivity_verify_s, e164number));
|
||||
title.clearAnimation();
|
||||
title.setTranslationX(title.getWidth());
|
||||
title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
|
||||
|
@ -557,13 +730,13 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
subtitle.animate().translationX(-1 * subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
SpannableString subtitleDescription = new SpannableString(String.format("Please enter the verification code sent to %s.", e164number));
|
||||
SpannableString wrongNumber = new SpannableString("Wrong number?");
|
||||
SpannableString subtitleDescription = new SpannableString(getString(R.string.RegistrationActivity_please_enter_the_verification_code_sent_to_s, e164number));
|
||||
SpannableString wrongNumber = new SpannableString(getString(R.string.RegistrationActivity_wrong_number));
|
||||
|
||||
ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
displayInitialView(e164number);
|
||||
displayInitialView(false);
|
||||
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
|
||||
}
|
||||
|
||||
|
@ -651,6 +824,12 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
|||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(FullBackupBase.BackupEvent event) {
|
||||
if (event.getCount() == 0) restoreBackupProgress.setText(R.string.RegistrationActivity_checking);
|
||||
else restoreBackupProgress.setText(getString(R.string.RegistrationActivity_d_messages_so_far, event.getCount()));
|
||||
}
|
||||
|
||||
private class ChallengeReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
|
86
src/org/thoughtcrime/securesms/backup/BackupDialog.java
Normal file
86
src/org/thoughtcrime/securesms/backup/BackupDialog.java
Normal file
|
@ -0,0 +1,86 @@
|
|||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class BackupDialog {
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
String[] password = BackupUtil.generateBackupPassphrase();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||
.setView(R.layout.backup_enable_dialog)
|
||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(created -> {
|
||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(v -> {
|
||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||
if (confirmationCheckBox.isChecked()) {
|
||||
TextSecurePreferences.setBackupPassphrase(context, Util.join(password, " "));
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
|
||||
preference.setChecked(true);
|
||||
created.dismiss();
|
||||
} else {
|
||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
|
||||
TextView textView = dialog.findViewById(R.id.confirmation_text);
|
||||
|
||||
((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
|
||||
((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
|
||||
((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
|
||||
|
||||
((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
|
||||
((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
|
||||
((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
|
||||
|
||||
textView.setOnClickListener(v -> checkBox.toggle());
|
||||
|
||||
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
|
||||
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", Util.join(password, " ")));
|
||||
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_delete_backups)
|
||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
||||
TextSecurePreferences.setBackupPassphrase(context, null);
|
||||
TextSecurePreferences.setBackupEnabled(context, false);
|
||||
BackupUtil.deleteAllBackups();
|
||||
preference.setChecked(false);
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
4079
src/org/thoughtcrime/securesms/backup/BackupProtos.java
Normal file
4079
src/org/thoughtcrime/securesms/backup/BackupProtos.java
Normal file
File diff suppressed because it is too large
Load diff
62
src/org/thoughtcrime/securesms/backup/FullBackupBase.java
Normal file
62
src/org/thoughtcrime/securesms/backup/FullBackupBase.java
Normal file
|
@ -0,0 +1,62 @@
|
|||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public abstract class FullBackupBase {
|
||||
|
||||
private static final String TAG = FullBackupBase.class.getSimpleName();
|
||||
|
||||
protected static @NonNull byte[] getBackupKey(@NonNull String passphrase) {
|
||||
try {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] input = passphrase.replace(" ", "").getBytes();
|
||||
byte[] hash = input;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
for (int i=0;i<250000;i++) {
|
||||
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
||||
digest.update(hash);
|
||||
hash = digest.digest(input);
|
||||
}
|
||||
Log.w(TAG, "Generated: " + (System.currentTimeMillis()- start));
|
||||
|
||||
return ByteUtil.trim(hash, 32);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupEvent {
|
||||
public enum Type {
|
||||
PROGRESS,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final int count;
|
||||
|
||||
BackupEvent(Type type, int count) {
|
||||
this.type = type;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
312
src/org/thoughtcrime/securesms/backup/FullBackupExporter.java
Normal file
312
src/org/thoughtcrime/securesms/backup/FullBackupExporter.java
Normal file
|
@ -0,0 +1,312 @@
|
|||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = FullBackupExporter.class.getSimpleName();
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull File output,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
byte[] key = getBackupKey(passphrase);
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, key);
|
||||
outputStream.writeDatabaseVersion(input.getVersion());
|
||||
|
||||
List<String> tables = exportSchema(input, outputStream);
|
||||
int count = 0;
|
||||
|
||||
for (String table : tables) {
|
||||
if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count);
|
||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, null, cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count);
|
||||
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
|
||||
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
|
||||
!table.equals(SessionDatabase.TABLE_NAME))
|
||||
{
|
||||
count = exportTable(table, input, outputStream, null, null, count);
|
||||
}
|
||||
}
|
||||
|
||||
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
|
||||
if (++count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
|
||||
outputStream.write(preference);
|
||||
}
|
||||
|
||||
outputStream.writeEnd();
|
||||
outputStream.close();
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
||||
}
|
||||
|
||||
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
|
||||
throws IOException
|
||||
{
|
||||
List<String> tables = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String sql = cursor.getString(0);
|
||||
String name = cursor.getString(1);
|
||||
String type = cursor.getString(2);
|
||||
|
||||
if (sql != null) {
|
||||
if ("table".equals(type)) {
|
||||
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP TABLE IF EXISTS " + name).build());
|
||||
tables.add(name);
|
||||
} else if ("index".equals(type)) {
|
||||
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP INDEX IF EXISTS " + name).build());
|
||||
}
|
||||
|
||||
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static int exportTable(@NonNull String table,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull BackupFrameOutputStream outputStream,
|
||||
@Nullable Predicate<Cursor> predicate,
|
||||
@Nullable Consumer<Cursor> postProcess,
|
||||
int count)
|
||||
throws IOException
|
||||
{
|
||||
String template = "INSERT INTO " + table + " VALUES ";
|
||||
|
||||
try (Cursor cursor = input.rawQuery("SELECT * FROM " + table, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
if (++count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
|
||||
|
||||
if (predicate == null || predicate.test(cursor)) {
|
||||
StringBuilder statement = new StringBuilder(template);
|
||||
|
||||
statement.append('(');
|
||||
|
||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
||||
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
|
||||
statement.append('\'');
|
||||
statement.append(cursor.getString(i));
|
||||
statement.append('\'');
|
||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
|
||||
statement.append(cursor.getFloat(i));
|
||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
|
||||
statement.append(cursor.getLong(i));
|
||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
|
||||
statement.append("x'");
|
||||
statement.append(Hex.toStringCondensed(cursor.getBlob(i)));
|
||||
statement.append('\'');
|
||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
|
||||
statement.append("NULL");
|
||||
} else {
|
||||
throw new AssertionError("unknown type?" + cursor.getType(i));
|
||||
}
|
||||
|
||||
if (i < cursor.getColumnCount()-1) {
|
||||
statement.append(',');
|
||||
}
|
||||
}
|
||||
|
||||
statement.append(')');
|
||||
|
||||
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(statement.toString()).build());
|
||||
|
||||
if (postProcess != null) postProcess.accept(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
|
||||
try {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
|
||||
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
|
||||
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
|
||||
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
InputStream inputStream;
|
||||
|
||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
|
||||
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BackupFrameOutputStream {
|
||||
|
||||
private final OutputStream outputStream;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
private final byte[] cipherKey;
|
||||
private final byte[] macKey;
|
||||
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupFrameOutputStream(@NonNull File output, @NonNull byte[] key) throws IOException {
|
||||
try {
|
||||
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
|
||||
byte[][] split = ByteUtil.split(derived, 32, 32);
|
||||
|
||||
this.cipherKey = split[0];
|
||||
this.macKey = split[1];
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.outputStream = new FileOutputStream(output);
|
||||
this.iv = Util.getSecretBytes(16);
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
|
||||
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
||||
|
||||
byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder().setIv(ByteString.copyFrom(iv))).build().toByteArray();
|
||||
|
||||
outputStream.write(Conversions.intToByteArray(header.length));
|
||||
outputStream.write(header);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(BackupProtos.SharedPreference preference) throws IOException {
|
||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
|
||||
}
|
||||
|
||||
public void write(BackupProtos.SqlStatement statement) throws IOException {
|
||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
|
||||
}
|
||||
|
||||
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
|
||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
||||
.setAttachment(BackupProtos.Attachment.newBuilder()
|
||||
.setRowId(attachmentId.getRowId())
|
||||
.setAttachmentId(attachmentId.getUniqueId())
|
||||
.setLength(Util.toIntExact(size))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
byte[] ciphertext = cipher.update(buffer, 0, read);
|
||||
outputStream.write(ciphertext);
|
||||
mac.update(ciphertext);
|
||||
}
|
||||
|
||||
byte[] remainder = cipher.doFinal();
|
||||
outputStream.write(remainder);
|
||||
mac.update(remainder);
|
||||
|
||||
byte[] attachmentDigest = mac.doFinal();
|
||||
outputStream.write(attachmentDigest, 0, 10);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void writeDatabaseVersion(int version) throws IOException {
|
||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
||||
.setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
|
||||
.build());
|
||||
}
|
||||
|
||||
void writeEnd() throws IOException {
|
||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
|
||||
}
|
||||
|
||||
private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
|
||||
byte[] frameMac = mac.doFinal(frameCiphertext);
|
||||
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);
|
||||
|
||||
out.write(length);
|
||||
out.write(frameCiphertext);
|
||||
out.write(frameMac, 0, 10);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
240
src/org/thoughtcrime/securesms/backup/FullBackupImporter.java
Normal file
240
src/org/thoughtcrime/securesms/backup/FullBackupImporter.java
Normal file
|
@ -0,0 +1,240 @@
|
|||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Pair;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class FullBackupImporter extends FullBackupBase {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = FullBackupImporter.class.getSimpleName();
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
byte[] key = getBackupKey(passphrase);
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, key);
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
BackupFrame frame;
|
||||
|
||||
while (!(frame = inputStream.readFrame()).getEnd()) {
|
||||
if (count++ % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
|
||||
|
||||
if (frame.hasVersion()) processVersion(db, frame.getVersion());
|
||||
else if (frame.hasStatement()) processStatement(db, frame.getStatement());
|
||||
else if (frame.hasPreference()) processPreference(context, frame.getPreference());
|
||||
else if (frame.hasAttachment()) processAttachment(context, attachmentSecret, db, frame.getAttachment(), inputStream);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
||||
}
|
||||
|
||||
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) {
|
||||
db.setVersion(version.getVersion());
|
||||
}
|
||||
|
||||
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
|
||||
db.execSQL(statement.getStatement());
|
||||
}
|
||||
|
||||
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
|
||||
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
inputStream.readAttachmentTo(output.second, attachment.getLength());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
|
||||
contentValues.put(AttachmentDatabase.THUMBNAIL, (String)null);
|
||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
|
||||
|
||||
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
|
||||
AttachmentDatabase.ROW_ID + " = ? AND " + AttachmentDatabase.UNIQUE_ID + " = ?",
|
||||
new String[] {String.valueOf(attachment.getRowId()), String.valueOf(attachment.getAttachmentId())});
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private static void processPreference(@NonNull Context context, SharedPreference preference) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
|
||||
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
|
||||
}
|
||||
|
||||
private static class BackupRecordInputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
private final byte[] cipherKey;
|
||||
private final byte[] macKey;
|
||||
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupRecordInputStream(@NonNull File file, @NonNull byte[] key) throws IOException {
|
||||
try {
|
||||
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
|
||||
byte[][] split = ByteUtil.split(derived, 32, 32);
|
||||
|
||||
this.cipherKey = split[0];
|
||||
this.macKey = split[1];
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.in = new FileInputStream(file);
|
||||
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
||||
|
||||
byte[] headerLengthBytes = new byte[4];
|
||||
Util.readFully(in, headerLengthBytes);
|
||||
|
||||
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
|
||||
byte[] headerFrame = new byte[headerLength];
|
||||
Util.readFully(in, headerFrame);
|
||||
|
||||
BackupFrame frame = BackupFrame.parseFrom(headerFrame);
|
||||
|
||||
if (!frame.hasHeader()) {
|
||||
throw new IOException("Backup stream does not start with header!");
|
||||
}
|
||||
|
||||
BackupProtos.Header header = frame.getHeader();
|
||||
|
||||
this.iv = header.getIv().toByteArray();
|
||||
|
||||
if (iv.length != 16) {
|
||||
throw new IOException("Invalid IV length!");
|
||||
}
|
||||
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
BackupFrame readFrame() throws IOException {
|
||||
return readFrame(in);
|
||||
}
|
||||
|
||||
void readAttachmentTo(OutputStream out, int length) throws IOException {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
while (length > 0) {
|
||||
int read = in.read(buffer, 0, Math.min(buffer.length, length));
|
||||
if (read == -1) throw new IOException("File ended early!");
|
||||
|
||||
mac.update(buffer, 0, read);
|
||||
|
||||
byte[] plaintext = cipher.update(buffer, 0, read);
|
||||
out.write(plaintext, 0, plaintext.length);
|
||||
|
||||
length -= read;
|
||||
}
|
||||
|
||||
out.close();
|
||||
|
||||
byte[] ourMac = mac.doFinal();
|
||||
byte[] theirMac = new byte[10];
|
||||
|
||||
try {
|
||||
Util.readFully(in, theirMac);
|
||||
} catch (IOException e) {
|
||||
//destination.delete();
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
//destination.delete();
|
||||
throw new IOException("Bad MAC");
|
||||
}
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BackupFrame readFrame(InputStream in) throws IOException {
|
||||
try {
|
||||
byte[] length = new byte[4];
|
||||
Util.readFully(in, length);
|
||||
|
||||
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
|
||||
Util.readFully(in, frame);
|
||||
|
||||
byte[] theirMac = new byte[10];
|
||||
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
|
||||
|
||||
mac.update(frame, 0, frame.length - 10);
|
||||
byte[] ourMac = mac.doFinal();
|
||||
|
||||
if (MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new IOException("Bad MAC");
|
||||
}
|
||||
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
|
||||
|
||||
return BackupFrame.parseFrom(plaintext);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,12 +4,16 @@ import android.annotation.TargetApi;
|
|||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.v7.preference.CheckBoxPreference;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class SwitchPreferenceCompat extends CheckBoxPreference {
|
||||
|
||||
private Preference.OnPreferenceClickListener listener;
|
||||
|
||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setLayoutRes();
|
||||
|
@ -34,4 +38,16 @@ public class SwitchPreferenceCompat extends CheckBoxPreference {
|
|||
private void setLayoutRes() {
|
||||
setWidgetLayoutResource(R.layout.switch_compat_preference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPreferenceClickListener(Preference.OnPreferenceClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
if (listener == null || !listener.onPreferenceClick(this)) {
|
||||
super.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
|
@ -22,6 +22,7 @@ import android.content.SharedPreferences;
|
|||
import android.content.SharedPreferences.Editor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
|
@ -31,6 +32,8 @@ import org.whispersystems.libsignal.ecc.ECKeyPair;
|
|||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class for working with identity keys.
|
||||
|
@ -40,6 +43,7 @@ import java.io.IOException;
|
|||
|
||||
public class IdentityKeyUtil {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||
|
||||
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
|
||||
|
@ -107,6 +111,23 @@ public class IdentityKeyUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
|
||||
return new LinkedList<BackupProtos.SharedPreference>() {{
|
||||
add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build());
|
||||
add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build());
|
||||
}};
|
||||
}
|
||||
|
||||
private static boolean hasLegacyIdentityKeys(Context context) {
|
||||
return
|
||||
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
|
||||
|
|
|
@ -65,27 +65,29 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
private static final String TAG = AttachmentDatabase.class.getSimpleName();
|
||||
|
||||
static final String TABLE_NAME = "part";
|
||||
static final String ROW_ID = "_id";
|
||||
public static final String TABLE_NAME = "part";
|
||||
public static final String ROW_ID = "_id";
|
||||
public static final String ATTACHMENT_ID_ALIAS = "attachment_id";
|
||||
static final String MMS_ID = "mid";
|
||||
static final String CONTENT_TYPE = "ct";
|
||||
static final String NAME = "name";
|
||||
static final String CONTENT_DISPOSITION = "cd";
|
||||
static final String CONTENT_LOCATION = "cl";
|
||||
static final String DATA = "_data";
|
||||
public static final String DATA = "_data";
|
||||
static final String TRANSFER_STATE = "pending_push";
|
||||
static final String SIZE = "data_size";
|
||||
public static final String SIZE = "data_size";
|
||||
static final String FILE_NAME = "file_name";
|
||||
static final String THUMBNAIL = "thumbnail";
|
||||
public static final String THUMBNAIL = "thumbnail";
|
||||
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
|
||||
public static final String UNIQUE_ID = "unique_id";
|
||||
static final String DIGEST = "digest";
|
||||
static final String VOICE_NOTE = "voice_note";
|
||||
public static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
|
||||
private static final String DATA_RANDOM = "data_random";
|
||||
public static final String DATA_RANDOM = "data_random";
|
||||
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
|
||||
|
||||
public static final String DIRECTORY = "parts";
|
||||
|
||||
public static final int TRANSFER_PROGRESS_DONE = 0;
|
||||
public static final int TRANSFER_PROGRESS_STARTED = 1;
|
||||
public static final int TRANSFER_PROGRESS_PENDING = 2;
|
||||
|
@ -256,7 +258,7 @@ public class AttachmentDatabase extends Database {
|
|||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, null, null);
|
||||
|
||||
File attachmentsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
|
||||
File attachmentsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
|
||||
File[] attachments = attachmentsDirectory.listFiles();
|
||||
|
||||
for (File attachment : attachments) {
|
||||
|
@ -459,7 +461,7 @@ public class AttachmentDatabase extends Database {
|
|||
throws MmsException
|
||||
{
|
||||
try {
|
||||
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
|
||||
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
|
||||
return setAttachmentData(dataFile, in);
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -130,6 +130,14 @@ public class DatabaseFactory {
|
|||
return getInstance(context).sessionDatabase;
|
||||
}
|
||||
|
||||
public static SQLiteDatabase getBackupDatabase(Context context) {
|
||||
return getInstance(context).databaseHelper.getReadableDatabase();
|
||||
}
|
||||
|
||||
public static void upgradeRestored(Context context, SQLiteDatabase database){
|
||||
getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1);
|
||||
}
|
||||
|
||||
private DatabaseFactory(@NonNull Context context) {
|
||||
SQLiteDatabase.loadLibs(context);
|
||||
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PlaintextBackupExporter {
|
||||
|
||||
private static final String FILENAME = "SignalPlaintextBackup.xml";
|
||||
|
||||
public static void exportPlaintextToSd(Context context)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
exportPlaintext(context);
|
||||
}
|
||||
|
||||
public static File getPlaintextExportFile() throws NoExternalStorageException {
|
||||
return new File(StorageUtil.getBackupDir(), FILENAME);
|
||||
}
|
||||
|
||||
private static void exportPlaintext(Context context)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
int count = database.getMessageCount();
|
||||
XmlBackup.Writer writer = new XmlBackup.Writer(getPlaintextExportFile().getAbsolutePath(), count);
|
||||
|
||||
|
||||
SmsMessageRecord record;
|
||||
|
||||
SmsDatabase.Reader reader = null;
|
||||
int skip = 0;
|
||||
int ROW_LIMIT = 500;
|
||||
|
||||
do {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
|
||||
reader = database.readerFor(database.getMessages(skip, ROW_LIMIT));
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
XmlBackup.XmlBackupItem item =
|
||||
new XmlBackup.XmlBackupItem(0, record.getIndividualRecipient().getAddress().serialize(),
|
||||
record.getIndividualRecipient().getName(),
|
||||
record.getDateReceived(),
|
||||
MmsSmsColumns.Types.translateToSystemBaseType(record.getType()),
|
||||
null, record.getDisplayBody().toString(), null,
|
||||
1, record.getDeliveryStatus());
|
||||
|
||||
writer.writeItem(item);
|
||||
}
|
||||
|
||||
skip += ROW_LIMIT;
|
||||
} while (reader.getCount() > 0);
|
||||
|
||||
writer.close();
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -72,7 +73,7 @@ public class PlaintextBackupImporter {
|
|||
}
|
||||
|
||||
private static File getPlaintextExportFile() throws NoExternalStorageException {
|
||||
File backup = PlaintextBackupExporter.getPlaintextExportFile();
|
||||
File backup = new File(StorageUtil.getLegacyBackupDirectory(), "SignalPlaintextBackup.xml");
|
||||
File oldBackup = new File(Environment.getExternalStorageDirectory(), "TextSecurePlaintextBackup.xml");
|
||||
|
||||
return !backup.exists() && oldBackup.exists() ? oldBackup : backup;
|
||||
|
|
94
src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java
Normal file
94
src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java
Normal file
|
@ -0,0 +1,94 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.backup.FullBackupExporter;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalBackupJob extends ContextJob {
|
||||
|
||||
private static final String TAG = LocalBackupJob.class.getSimpleName();
|
||||
|
||||
public LocalBackupJob(@NonNull Context context) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withGroupId("__LOCAL_BACKUP__")
|
||||
.withWakeLock(true, 10, TimeUnit.SECONDS)
|
||||
.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {}
|
||||
|
||||
@Override
|
||||
public void onRun() throws NoExternalStorageException, IOException {
|
||||
Log.w(TAG, "Executing backup job...");
|
||||
|
||||
if (!Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
throw new IOException("No external storage permission!");
|
||||
}
|
||||
|
||||
GenericForegroundService.startForegroundTask(context, "Creating backup");
|
||||
|
||||
try {
|
||||
String backupPassword = TextSecurePreferences.getBackupPassphrase(context);
|
||||
File backupDirectory = StorageUtil.getBackupDirectory();
|
||||
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
|
||||
String fileName = String.format("signal-%s.backup", timestamp);
|
||||
File backupFile = new File(backupDirectory, fileName);
|
||||
|
||||
if (backupFile.exists()) {
|
||||
throw new IOException("Backup file already exists?");
|
||||
}
|
||||
|
||||
if (backupPassword == null) {
|
||||
throw new IOException("Backup password is null");
|
||||
}
|
||||
|
||||
File tempFile = File.createTempFile("backup", "tmp", context.getExternalCacheDir());
|
||||
|
||||
FullBackupExporter.export(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
DatabaseFactory.getBackupDatabase(context),
|
||||
tempFile,
|
||||
backupPassword);
|
||||
|
||||
if (!tempFile.renameTo(backupFile)) {
|
||||
tempFile.delete();
|
||||
throw new IOException("Renaming temporary backup file failed!");
|
||||
}
|
||||
|
||||
BackupUtil.deleteOldBackups();
|
||||
} finally {
|
||||
GenericForegroundService.stopForegroundTask(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,6 +21,7 @@ import android.content.res.Resources.Theme;
|
|||
import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
|
@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
|||
|
||||
public class ImageSlide extends Slide {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ImageSlide.class.getSimpleName();
|
||||
|
||||
public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
|
@ -43,6 +45,14 @@ public class ImageSlide extends Slide {
|
|||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getThumbnailUri() {
|
||||
Uri thumbnailUri = super.getThumbnailUri();
|
||||
|
||||
if (thumbnailUri == null) return getUri();
|
||||
else return thumbnailUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
|
|
|
@ -223,7 +223,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
|
|||
Intent nextIntent = new Intent(getActivity(), ApplicationPreferencesActivity.class);
|
||||
|
||||
Intent intent = new Intent(getActivity(), RegistrationActivity.class);
|
||||
intent.putExtra("cancel_button", true);
|
||||
intent.putExtra(RegistrationActivity.RE_REGISTRATION_EXTRA, true);
|
||||
intent.putExtra("next_intent", nextIntent);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.preference.EditTextPreference;
|
||||
|
@ -11,13 +14,26 @@ import android.support.v7.preference.Preference;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupDialog;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase.BackupEvent;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Trimmer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
@ -41,7 +57,14 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH)
|
||||
.setOnPreferenceChangeListener(new TrimLengthValidationListener());
|
||||
|
||||
findPreference(TextSecurePreferences.BACKUP_ENABLED)
|
||||
.setOnPreferenceClickListener(new BackupClickListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setOnPreferenceClickListener(new BackupCreateListener());
|
||||
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF));
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,6 +77,38 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
super.onResume();
|
||||
((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__chats);
|
||||
setMediaDownloadSummaries();
|
||||
setBackupSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(BackupEvent event) {
|
||||
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
|
||||
|
||||
if (event.getType() == BackupEvent.Type.PROGRESS) {
|
||||
preference.setEnabled(false);
|
||||
preference.setSummary(getString(R.string.ChatsPreferenceFragment_in_progress));
|
||||
preference.setProgress(event.getCount());
|
||||
} else if (event.getType() == BackupEvent.Type.FINISHED) {
|
||||
preference.setEnabled(true);
|
||||
preference.setProgressVisible(false);
|
||||
setBackupSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupSummary() {
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setSummary(String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), BackupUtil.getLastBackupTime(getContext(), Locale.US)));
|
||||
}
|
||||
|
||||
private void setMediaDownloadSummaries() {
|
||||
|
@ -78,6 +133,46 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
: TextUtils.join(", ", outValues);
|
||||
}
|
||||
|
||||
private class BackupClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!((SwitchPreferenceCompat)preference).isChecked()) {
|
||||
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
} else {
|
||||
BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
}
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupCreateListener implements Preference.OnPreferenceClickListener {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
Log.w(TAG, "Queing backup...");
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new LocalBackupJob(getContext()));
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class TrimNowClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.PreferenceViewHolder;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class ProgressPreference extends Preference {
|
||||
|
||||
private View container;
|
||||
private TextView progressText;
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setWidgetLayoutResource(R.layout.preference_widget_progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
|
||||
this.container = view.findViewById(R.id.container);
|
||||
this.progressText = (TextView) view.findViewById(R.id.progress_text);
|
||||
|
||||
this.container.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setProgress(int count) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
progressText.setText(getContext().getString(R.string.ProgressPreference_d_messages_so_far, count));
|
||||
}
|
||||
|
||||
public void setProgressVisible(boolean visible) {
|
||||
container.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalBackupListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getNextBackupTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new LocalBackupJob(context));
|
||||
}
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setNextBackupTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
new LocalBackupListener().onReceive(context, new Intent());
|
||||
}
|
||||
}
|
||||
}
|
160
src/org/thoughtcrime/securesms/util/BackupUtil.java
Normal file
160
src/org/thoughtcrime/securesms/util/BackupUtil.java
Normal file
|
@ -0,0 +1,160 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
public class BackupUtil {
|
||||
|
||||
private static final String TAG = BackupUtil.class.getSimpleName();
|
||||
|
||||
public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
|
||||
try {
|
||||
BackupInfo backup = getLatestBackup();
|
||||
|
||||
if (backup == null) return context.getString(R.string.BackupUtil_never);
|
||||
else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp());
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, e);
|
||||
return context.getString(R.string.BackupUtil_unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable BackupInfo getLatestBackup() throws NoExternalStorageException {
|
||||
File backupDirectory = StorageUtil.getBackupDirectory();
|
||||
File[] backups = backupDirectory.listFiles();
|
||||
BackupInfo latestBackup = null;
|
||||
|
||||
for (File backup : backups) {
|
||||
long backupTimestamp = getBackupTimestamp(backup);
|
||||
|
||||
if (latestBackup == null || (backupTimestamp != -1 && backupTimestamp > latestBackup.getTimestamp())) {
|
||||
latestBackup = new BackupInfo(backupTimestamp, backup.length(), backup);
|
||||
}
|
||||
}
|
||||
|
||||
return latestBackup;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static void deleteAllBackups() {
|
||||
try {
|
||||
File backupDirectory = StorageUtil.getBackupDirectory();
|
||||
File[] backups = backupDirectory.listFiles();
|
||||
|
||||
for (File backup : backups) {
|
||||
if (backup.isFile()) backup.delete();
|
||||
}
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteOldBackups() {
|
||||
try {
|
||||
File backupDirectory = StorageUtil.getBackupDirectory();
|
||||
File[] backups = backupDirectory.listFiles();
|
||||
|
||||
if (backups != null && backups.length > 5) {
|
||||
Arrays.sort(backups, (left, right) -> {
|
||||
long leftTimestamp = getBackupTimestamp(left);
|
||||
long rightTimestamp = getBackupTimestamp(right);
|
||||
|
||||
if (leftTimestamp == -1 && rightTimestamp == -1) return 0;
|
||||
else if (leftTimestamp == -1) return 1;
|
||||
else if (rightTimestamp == -1) return -1;
|
||||
|
||||
return (int)(rightTimestamp - leftTimestamp);
|
||||
});
|
||||
|
||||
for (int i=5;i<backups.length;i++) {
|
||||
Log.w(TAG, "Deleting: " + backups[i].getAbsolutePath());
|
||||
|
||||
if (!backups[i].delete()) {
|
||||
Log.w(TAG, "Delete failed: " + backups[i].getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String[] generateBackupPassphrase() {
|
||||
String[] result = new String[6];
|
||||
byte[] random = new byte[30];
|
||||
|
||||
new SecureRandom().nextBytes(random);
|
||||
|
||||
for (int i=0;i<30;i+=5) {
|
||||
result[i/5] = String.format("%05d", ByteUtil.byteArray5ToLong(random, i) % 100000);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static long getBackupTimestamp(File backup) {
|
||||
String name = backup.getName();
|
||||
String[] prefixSuffix = name.split("[.]");
|
||||
|
||||
if (prefixSuffix.length == 2) {
|
||||
String[] parts = prefixSuffix[0].split("\\-");
|
||||
|
||||
if (parts.length == 7) {
|
||||
try {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.YEAR, Integer.parseInt(parts[1]));
|
||||
calendar.set(Calendar.MONTH, Integer.parseInt(parts[2]) - 1);
|
||||
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(parts[3]));
|
||||
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(parts[4]));
|
||||
calendar.set(Calendar.MINUTE, Integer.parseInt(parts[5]));
|
||||
calendar.set(Calendar.SECOND, Integer.parseInt(parts[6]));
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
return calendar.getTimeInMillis();
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static class BackupInfo {
|
||||
|
||||
private final long timestamp;
|
||||
private final long size;
|
||||
private final File file;
|
||||
|
||||
BackupInfo(long timestamp, long size, File file) {
|
||||
this.timestamp = timestamp;
|
||||
this.size = size;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,12 +21,11 @@ import android.os.Build;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -34,6 +33,9 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class DateUtils extends android.text.format.DateUtils {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DateUtils.class.getSimpleName();
|
||||
|
||||
private static boolean isWithin(final long millis, final long span, final TimeUnit unit) {
|
||||
return System.currentTimeMillis() - millis <= unit.toMillis(span);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,27 @@ import java.io.File;
|
|||
|
||||
public class StorageUtil
|
||||
{
|
||||
|
||||
public static File getBackupDirectory() throws NoExternalStorageException {
|
||||
File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
if (!storage.canWrite()) {
|
||||
throw new NoExternalStorageException();
|
||||
}
|
||||
|
||||
File signal = new File(storage, "Signal");
|
||||
File backups = new File(signal, "Backups");
|
||||
|
||||
if (!backups.exists()) {
|
||||
if (!backups.mkdirs()) {
|
||||
throw new NoExternalStorageException("Unable to create backup directory...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
private static File getSignalStorageDir() throws NoExternalStorageException {
|
||||
final File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
|
@ -31,7 +52,7 @@ public class StorageUtil
|
|||
return storage.canWrite();
|
||||
}
|
||||
|
||||
public static File getBackupDir() throws NoExternalStorageException {
|
||||
public static File getLegacyBackupDirectory() throws NoExternalStorageException {
|
||||
return getSignalStorageDir();
|
||||
}
|
||||
|
||||
|
|
|
@ -137,6 +137,35 @@ public class TextSecurePreferences {
|
|||
private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id";
|
||||
private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id";
|
||||
|
||||
public static final String BACKUP_ENABLED = "pref_backup_enabled";
|
||||
private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase";
|
||||
private static final String BACKUP_TIME = "pref_backup_next_time";
|
||||
public static final String BACKUP_NOW = "pref_backup_create";
|
||||
|
||||
public static void setBackupPassphrase(@NonNull Context context, @Nullable String passphrase) {
|
||||
setStringPreference(context, BACKUP_PASSPHRASE, passphrase);
|
||||
}
|
||||
|
||||
public static @Nullable String getBackupPassphrase(@NonNull Context context) {
|
||||
return getStringPreference(context, BACKUP_PASSPHRASE, null);
|
||||
}
|
||||
|
||||
public static void setBackupEnabled(@NonNull Context context, boolean value) {
|
||||
setBooleanPreference(context, BACKUP_ENABLED, value);
|
||||
}
|
||||
|
||||
public static boolean isBackupEnabled(@NonNull Context context) {
|
||||
return getBooleanPreference(context, BACKUP_ENABLED, false);
|
||||
}
|
||||
|
||||
public static void setNextBackupTime(@NonNull Context context, long time) {
|
||||
setLongPreference(context, BACKUP_TIME, time);
|
||||
}
|
||||
|
||||
public static long getNextBackupTime(@NonNull Context context) {
|
||||
return getLongPreference(context, BACKUP_TIME, -1);
|
||||
}
|
||||
|
||||
public static int getNextPreKeyId(@NonNull Context context) {
|
||||
return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE));
|
||||
}
|
||||
|
|
|
@ -207,6 +207,18 @@ public class Util {
|
|||
return TextSecurePreferences.getLocalNumber(context).equals(address.toPhoneString());
|
||||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = in.read(buffer, offset, buffer.length - offset);
|
||||
if (read == -1) throw new IOException("Stream ended early");
|
||||
|
||||
if (read + offset < buffer.length) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
|
@ -351,11 +363,7 @@ public class Util {
|
|||
}
|
||||
|
||||
public static SecureRandom getSecureRandom() {
|
||||
try {
|
||||
return SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new SecureRandom();
|
||||
}
|
||||
|
||||
public static int getDaysTillBuildExpiry() {
|
||||
|
|
Loading…
Add table
Reference in a new issue