Support for full backup/restore to sdcard

This commit is contained in:
Moxie Marlinspike 2018-02-26 09:58:18 -08:00
parent 9f6b761d98
commit 24e573e537
41 changed files with 5884 additions and 269 deletions

43
protobuf/Backups.proto Normal file
View 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;
}

View File

@ -1,3 +1,3 @@
all:
protoc --java_out=../src/ WebRtcData.proto
protoc --java_out=../src/ WebRtcData.proto Backups.proto

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View 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>

View 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>

View File

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

View 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>

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

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

View File

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

View File

@ -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 &&

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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