Transfer Data UI

This commit is contained in:
Torsten Grote
2021-06-09 17:58:14 -03:00
parent ecba2a51d8
commit 928b951c25
28 changed files with 1155 additions and 231 deletions

View File

@@ -438,7 +438,7 @@
<activity
android:name="org.briarproject.briar.android.removabledrive.RemovableDriveActivity"
android:label="TODO Removable Drive"
android:label="@string/removable_drive_menu_title"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View File

@@ -38,6 +38,9 @@ import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.removabledrive.ChooserFragment;
import org.briarproject.briar.android.removabledrive.ReceiveFragment;
import org.briarproject.briar.android.removabledrive.SendFragment;
import org.briarproject.briar.android.settings.ConnectionsFragment;
import org.briarproject.briar.android.settings.NotificationsFragment;
import org.briarproject.briar.android.settings.SecurityFragment;
@@ -212,4 +215,10 @@ public interface AndroidComponent
void inject(SecurityFragment securityFragment);
void inject(NotificationsFragment notificationsFragment);
void inject(ChooserFragment chooserFragment);
void inject(SendFragment sendFragment);
void inject(ReceiveFragment receiveFragment);
}

View File

@@ -25,7 +25,6 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
import org.briarproject.bramble.plugin.file.AndroidRemovableDrivePluginFactory;
import org.briarproject.bramble.plugin.file.RemovableDriveModule;
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils;
@@ -43,6 +42,7 @@ import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.removabledrive.TransferDataModule;
import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.sharing.SharingModule;
@@ -93,7 +93,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
GroupListModule.class,
GroupConversationModule.class,
SharingModule.class,
RemovableDriveModule.class
TransferDataModule.class,
})
public class AppModule {

View File

@@ -14,7 +14,5 @@ public interface RequestCodes {
int REQUEST_ATTACH_IMAGE = 13;
int REQUEST_SAVE_ATTACHMENT = 14;
int REQUEST_AVATAR_IMAGE = 15;
int REQUEST_REMOVABLE_DRIVE_WRITE = 16;
int REQUEST_REMOVABLE_DRIVE_READ = 17;
}

View File

@@ -375,6 +375,10 @@ public class ConversationActivity extends BriarActivity
if (!featureFlags.shouldEnableConnectViaBluetooth()) {
menu.findItem(R.id.action_connect_via_bluetooth).setVisible(false);
}
// Transfer Data feature only supported on API 19+
if (SDK_INT >= 19) { // TODO also hide behind feature flag
menu.findItem(R.id.action_transfer_data).setVisible(true);
}
// enable alias and bluetooth action once available
observeOnce(viewModel.getContactItem(), this, contact -> {
menu.findItem(R.id.action_set_alias).setEnabled(true);
@@ -415,17 +419,17 @@ public class ConversationActivity extends BriarActivity
new BluetoothConnecterDialogFragment().show(fm,
BluetoothConnecterDialogFragment.TAG);
return true;
} else if (itemId == R.id.action_transfer_data) {
Intent intent = new Intent(this, RemovableDriveActivity.class);
intent.putExtra(CONTACT_ID, contactId.getInt());
startActivity(intent);
return true;
} else if (itemId == R.id.action_delete_all_messages) {
askToDeleteAllMessages();
return true;
} else if (itemId == R.id.action_social_remove_person) {
askToRemoveContact();
return true;
} else if (itemId == R.id.action_removable_drive_write) {
Intent intent = new Intent(this, RemovableDriveActivity.class);
intent.putExtra(CONTACT_ID, contactId.getInt());
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}

View File

@@ -0,0 +1,108 @@
package org.briarproject.briar.android.fragment;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.ImageViewCompat;
import androidx.fragment.app.Fragment;
/**
* A fragment to be used at the end of a user flow
* where the user should not have the option to go back.
* Here, we only show final information
* before finishing the related activity.
*/
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FinalFragment extends Fragment {
public static final String TAG = FinalFragment.class.getName();
public static final String ARG_TITLE = "title";
public static final String ARG_ICON = "icon";
public static final String ARG_ICON_TINT = "iconTint";
public static final String ARG_TEXT = "text";
public static FinalFragment newInstance(
@StringRes int title,
@DrawableRes int icon,
@ColorRes int iconTint,
@StringRes int text) {
FinalFragment f = new FinalFragment();
Bundle args = new Bundle();
args.putInt(ARG_TITLE, title);
args.putInt(ARG_ICON, icon);
args.putInt(ARG_ICON_TINT, iconTint);
args.putInt(ARG_TEXT, text);
f.setArguments(args);
return f;
}
protected Button buttonView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_final, container, false);
ImageView iconView = v.findViewById(R.id.iconView);
TextView titleView = v.findViewById(R.id.titleView);
TextView textView = v.findViewById(R.id.textView);
buttonView = v.findViewById(R.id.button);
Bundle args = requireArguments();
titleView.setText(args.getInt(ARG_TITLE));
iconView.setImageResource(args.getInt(ARG_ICON));
int color = getResources().getColor(args.getInt(ARG_ICON_TINT));
ColorStateList tint = ColorStateList.valueOf(color);
ImageViewCompat.setImageTintList(iconView, tint);
textView.setText(args.getInt(ARG_TEXT));
buttonView.setOnClickListener(view -> onBackButtonPressed());
AppCompatActivity a = (AppCompatActivity) requireActivity();
a.setTitle(args.getInt(ARG_TITLE));
ActionBar actionBar = a.getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(false);
}
a.getOnBackPressedDispatcher().addCallback(
getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onBackButtonPressed();
}
});
return v;
}
/**
* This is the action that the system back button
* and the button at the bottom will perform.
*/
protected void onBackButtonPressed() {
requireActivity().supportFinishAfterTransition();
}
}

View File

@@ -0,0 +1,70 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ChooserFragment extends Fragment {
public final static String TAG = ChooserFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RemovableDriveViewModel viewModel;
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(RemovableDriveViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_transfer_data_chooser,
container, false);
Button sendButton = v.findViewById(R.id.sendButton);
sendButton.setOnClickListener(i -> viewModel.startSendData());
Button receiveButton = v.findViewById(R.id.receiveButton);
receiveButton.setOnClickListener(i -> viewModel.startReceiveData());
return v;
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.removable_drive_menu_title);
if (viewModel.getState().getValue() != null) {
// we can't come back here now to start another action
// as we only support one per ViewModel instance
requireActivity().supportFinishAfterTransition();
}
}
}

View File

@@ -0,0 +1,54 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.FinalFragment;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ErrorFragment extends FinalFragment {
public static ErrorFragment newInstance(@StringRes int title,
@StringRes int text) {
ErrorFragment f = new ErrorFragment();
Bundle args = new Bundle();
args.putInt(ARG_TITLE, title);
args.putInt(ARG_ICON, R.drawable.alerts_and_states_error);
args.putInt(ARG_ICON_TINT, R.color.briar_red_500);
args.putInt(ARG_TEXT, text);
f.setArguments(args);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
buttonView.setText(R.string.try_again_button);
return v;
}
@Override
protected void onBackButtonPressed() {
// Re-create this activity when going back in failed state.
// This will also re-create the ViewModel, so we start fresh.
Intent i = requireActivity().getIntent();
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
}

View File

@@ -0,0 +1,106 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.GetContent;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReceiveFragment extends Fragment {
final static String TAG = ReceiveFragment.class.getName();
// TODO we can pass an extra named DocumentsContract.EXTRA_INITIAL_URI
// to have the file-picker start on the usb-stick -- if get hold of URI
// of the same. USB manager API?
// Overall, passing this extra requires extending the ready-made
// contracts and overriding createIntent.
private final ActivityResultLauncher<String> launcher =
registerForActivityResult(new GetContent(), this::onDocumentChosen);
@Inject
ViewModelProvider.Factory viewModelFactory;
RemovableDriveViewModel viewModel;
TextView introTextView;
Button button;
ProgressBar progressBar;
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(RemovableDriveViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_transfer_data_receive,
container, false);
introTextView = v.findViewById(R.id.introTextView);
progressBar = v.findViewById(R.id.progressBar);
button = v.findViewById(R.id.fileButton);
button.setOnClickListener(view ->
launcher.launch("*/*")
);
viewModel.getState()
.observe(getViewLifecycleOwner(), this::onStateChanged);
return v;
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.removable_drive_title_receive);
}
private void onStateChanged(TransferDataState state) {
if (state instanceof TransferDataState.NoDataToSend) {
throw new IllegalStateException();
} else if (state instanceof TransferDataState.Ready) {
button.setVisibility(VISIBLE);
} else if (state instanceof TransferDataState.TaskAvailable) {
button.setEnabled(false);
if (((TransferDataState.TaskAvailable) state).isOldTask) {
Toast.makeText(requireContext(),
R.string.removable_drive_ongoing, LENGTH_LONG).show();
}
progressBar.setVisibility(VISIBLE);
}
}
private void onDocumentChosen(@Nullable Uri uri) {
if (uri == null) return;
viewModel.importData(uri);
}
}

View File

@@ -1,51 +1,42 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask.State;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import org.briarproject.briar.android.fragment.FinalFragment;
import org.briarproject.briar.android.removabledrive.RemovableDriveViewModel.Action;
import org.briarproject.briar.android.removabledrive.TransferDataState.TaskAvailable;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.LiveData;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
// TODO 19 will be our requirement for sneakernet support, right. The file apis
// used require this.
@RequiresApi(api = 19)
@RequiresApi(19)
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RemovableDriveActivity extends BriarActivity
implements BaseFragmentListener {
public class RemovableDriveActivity extends BriarActivity {
@Inject
ViewModelProvider.Factory viewModelFactory;
private RemovableDriveViewModel viewModel;
private TextView text;
private Button writeButton;
private Button readButton;
@Override
public void injectActivity(ActivityComponent component) {
@@ -58,57 +49,23 @@ public class RemovableDriveActivity extends BriarActivity
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_removable_drive);
text = findViewById(R.id.sneaker_text);
writeButton = findViewById(R.id.sneaker_write);
readButton = findViewById(R.id.sneaker_read);
Intent intent = getIntent();
Intent intent = requireNonNull(getIntent());
int contactId = intent.getIntExtra(CONTACT_ID, -1);
if (contactId == -1) {
writeButton.setEnabled(false);
readButton.setEnabled(false);
return;
}
if (contactId == -1) throw new IllegalArgumentException("ContactId");
viewModel.setContactId(new ContactId(contactId));
// TODO we can pass an extra named DocumentsContract.EXTRA_INITIAL_URI
// to have the filepicker start on the usb-stick -- if get hold of URI
// of the same. USB manager API?
// Overall, passing this extra requires extending the ready-made
// contracts and overriding createIntent.
setContentView(R.layout.activity_fragment_container);
writeButton.setText("Write for contactId " + contactId);
ActivityResultLauncher<String> createDocument =
registerForActivityResult(
new ActivityResultContracts.CreateDocument(),
uri -> write(contactId, uri));
writeButton.setOnClickListener(
v -> createDocument.launch(viewModel.getFileName()));
viewModel.getActionEvent().observeEvent(this, this::onActionReceived);
viewModel.getState().observe(this, this::onStateChanged);
readButton.setText("Read for contactId " + contactId);
ActivityResultLauncher<String> getContent =
registerForActivityResult(
new ActivityResultContracts.GetContent(),
uri -> read(contactId, uri));
readButton.setOnClickListener(
v -> getContent.launch("application/octet-stream"));
LiveData<State> state;
state = viewModel.ongoingWrite(new ContactId(contactId));
if (state == null) {
writeButton.setEnabled(true);
} else {
say("\nOngoing write:");
writeButton.setEnabled(false);
state.observe(this, (taskState) -> handleState("write", taskState));
}
state = viewModel.ongoingRead(new ContactId(contactId));
if (state == null) {
readButton.setEnabled(true);
} else {
say("\nOngoing read:");
readButton.setEnabled(false);
state.observe(this, (taskState) -> handleState("read", taskState));
if (savedInstanceState == null) {
Fragment f = new ChooserFragment();
String tag = ChooserFragment.TAG;
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, tag)
.commit();
}
}
@@ -121,54 +78,56 @@ public class RemovableDriveActivity extends BriarActivity
return super.onOptionsItemSelected(item);
}
private void write(int contactId, @Nullable Uri uri) {
if (contactId == -1) {
throw new IllegalStateException();
}
if (uri == null) {
say("no URI picked for write");
return;
}
say("\nWriting to URI: " + uri);
writeButton.setEnabled(false);
LiveData<State> state = viewModel.write(new ContactId(contactId), uri);
state.observe(this, (taskState) -> handleState("write", taskState));
private void onActionReceived(Action action) {
Fragment f;
String tag;
if (action == Action.SEND) {
f = new SendFragment();
tag = SendFragment.TAG;
} else if (action == Action.RECEIVE) {
f = new ReceiveFragment();
tag = ReceiveFragment.TAG;
} else throw new AssertionError();
showFragment(getSupportFragmentManager(), f, tag);
}
private void read(int contactId, @Nullable Uri uri) {
if (contactId == -1) {
throw new IllegalStateException();
}
if (uri == null) {
say("no URI picked for read");
return;
}
say("\nReading from URI: " + uri);
readButton.setEnabled(false);
LiveData<State> state = viewModel.read(new ContactId(contactId), uri);
state.observe(this, (taskState) -> handleState("read", taskState));
}
private void handleState(String action, State taskState) {
say(String.format(Locale.getDefault(),
"%s: bytes done: %d of %d. %s. %s.",
action, taskState.getDone(), taskState.getTotal(),
taskState.isFinished() ? "Finished" : "Ongoing",
taskState.isFinished() ?
(taskState.isSuccess() ? "Success" : "Failed") : ".."));
if (taskState.isFinished()) {
if (action.equals("write")) {
writeButton.setEnabled(true);
} else if (action.equals("read")) {
readButton.setEnabled(true);
}
private void onStateChanged(TransferDataState state) {
if (!(state instanceof TaskAvailable)) return;
RemovableDriveTask.State s = ((TaskAvailable) state).state;
if (s.isFinished()) {
Action action =
requireNonNull(viewModel.getActionEvent().getLastValue());
Fragment f;
if (s.isSuccess()) f = getSuccessFragment(action);
else f = getErrorFragment(action);
showFragment(getSupportFragmentManager(), f, FinalFragment.TAG);
}
}
private void say(String txt) {
String time = new SimpleDateFormat("HH:mm:ss", Locale.getDefault())
.format(new Date());
txt = String.format("%s %s\n", time, txt);
text.setText(text.getText().toString().concat(txt));
private Fragment getSuccessFragment(Action action) {
@StringRes int title, text;
if (action == Action.SEND) {
title = R.string.removable_drive_success_send_title;
text = R.string.removable_drive_success_send_text;
} else if (action == Action.RECEIVE) {
title = R.string.removable_drive_success_receive_title;
text = R.string.removable_drive_success_receive_text;
} else throw new AssertionError();
return FinalFragment.newInstance(title,
R.drawable.ic_check_circle_outline, R.color.briar_brand_green,
text);
}
private Fragment getErrorFragment(Action action) {
@StringRes int title, text;
if (action == Action.SEND) {
title = R.string.removable_drive_error_send_title;
text = R.string.removable_drive_error_send_text;
} else if (action == Action.RECEIVE) {
title = R.string.removable_drive_error_receive_title;
text = R.string.removable_drive_error_receive_text;
} else throw new AssertionError();
return ErrorFragment.newInstance(title, text);
}
}

View File

@@ -10,92 +10,159 @@ import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask.State;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.Locale.US;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_URI;
@UiThread
@NotNullByDefault
class RemovableDriveViewModel extends AndroidViewModel {
private static final Logger LOG =
getLogger(RemovableDriveViewModel.class.getName());
enum Action {SEND, RECEIVE}
private final RemovableDriveManager manager;
private final ConcurrentHashMap<Consumer<State>, RemovableDriveTask>
observers = new ConcurrentHashMap<>();
private final MutableLiveEvent<Action> action = new MutableLiveEvent<>();
private final MutableLiveData<TransferDataState> state =
new MutableLiveData<>();
private final State initialState = new State(0, 0, false, false);
@Nullable
private ContactId contactId = null;
@Nullable
private RemovableDriveTask task = null;
@Nullable
private Consumer<State> taskObserver = null;
@Inject
RemovableDriveViewModel(Application app,
RemovableDriveManager removableDriveManager) {
super(app);
this.manager = removableDriveManager;
}
String getFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS", US);
return sdf.format(new Date());
}
LiveData<State> write(ContactId contactId, Uri uri) {
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
return observe(manager.startWriterTask(contactId, p));
}
LiveData<State> read(Uri uri) {
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
return observe(manager.startReaderTask(p));
}
@Nullable
LiveData<State> ongoingWrite() {
RemovableDriveTask task = manager.getCurrentWriterTask();
if (task == null) {
return null;
}
return observe(task);
}
@Nullable
LiveData<State> ongoingRead() {
RemovableDriveTask task = manager.getCurrentReaderTask();
if (task == null) {
return null;
}
return observe(task);
}
private LiveData<State> observe(RemovableDriveTask task) {
MutableLiveData<State> state = new MutableLiveData<>();
Consumer<State> observer = state::postValue;
task.addObserver(observer);
observers.put(observer, task);
return state;
}
@Override
protected void onCleared() {
for (Map.Entry<Consumer<State>, RemovableDriveTask> entry
: observers.entrySet()) {
entry.getValue().removeObserver(entry.getKey());
if (task != null) {
// when we have a task, we must have an observer for it
Consumer<State> observer = requireNonNull(taskObserver);
task.removeObserver(observer);
}
}
/**
* Set this as soon as it becomes available.
*/
void setContactId(ContactId contactId) {
this.contactId = contactId;
}
@UiThread
void startSendData() {
action.setEvent(Action.SEND);
// check if there is already a send/write task
task = manager.getCurrentWriterTask();
if (task == null) {
// TODO check if there is data to export now
// and only allow to continue if there is.
state.setValue(new TransferDataState.Ready());
} else {
// observe old task and start with initial state
taskObserver = s -> observeTask(s, true);
taskObserver.accept(initialState);
task.addObserver(taskObserver);
}
}
@UiThread
void startReceiveData() {
action.setEvent(Action.RECEIVE);
// check if there is already a receive/read task
task = manager.getCurrentReaderTask();
if (task == null) {
state.setValue(new TransferDataState.Ready());
} else {
// observe old task and start with initial state
taskObserver = s -> observeTask(s, true);
taskObserver.accept(initialState);
task.addObserver(taskObserver);
}
}
@UiThread
private void observeTask(RemovableDriveTask.State s, boolean isOldTask) {
state.setValue(new TransferDataState.TaskAvailable(s, isOldTask));
}
String getFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", US);
return sdf.format(new Date()) + ".zip";
}
/**
* Call this only when in {@link TransferDataState.Ready}.
*/
@UiThread
void exportData(Uri uri) {
// starting an action more than once is not supported for simplicity
if (task != null) throw new IllegalStateException();
// from now on, we are not re-usable
taskObserver = s -> observeTask(s, false);
taskObserver.accept(initialState);
// start the writer task for this contact and observe it
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
ContactId c = requireNonNull(contactId);
task = manager.startWriterTask(c, p);
task.addObserver(taskObserver);
}
/**
* Call this only when in {@link TransferDataState.Ready}.
*/
@UiThread
void importData(Uri uri) {
// starting an action more than once is not supported for simplicity
if (task != null) throw new IllegalStateException();
// from now on, we are not re-usable
taskObserver = s -> observeTask(s, false);
taskObserver.accept(initialState);
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
task = manager.startReaderTask(p);
task.addObserver(taskObserver);
}
LiveEvent<Action> getActionEvent() {
return action;
}
LiveData<TransferDataState> getState() {
return state;
}
}

View File

@@ -0,0 +1,114 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SendFragment extends Fragment {
final static String TAG = SendFragment.class.getName();
private final ActivityResultLauncher<String> launcher =
registerForActivityResult(new CreateDocument(),
this::onDocumentCreated);
@Inject
ViewModelProvider.Factory viewModelFactory;
RemovableDriveViewModel viewModel;
TextView introTextView;
Button button;
ProgressBar progressBar;
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(RemovableDriveViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_transfer_data_send,
container, false);
introTextView = v.findViewById(R.id.introTextView);
progressBar = v.findViewById(R.id.progressBar);
button = v.findViewById(R.id.fileButton);
button.setOnClickListener(view ->
launcher.launch(viewModel.getFileName())
);
viewModel.getState()
.observe(getViewLifecycleOwner(), this::onStateChanged);
return v;
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.removable_drive_title_send);
}
private void onStateChanged(TransferDataState state) {
if (state instanceof TransferDataState.NoDataToSend) {
introTextView.setText(R.string.removable_drive_send_no_data);
button.setVisibility(GONE);
} else if (state instanceof TransferDataState.Ready) {
button.setVisibility(VISIBLE);
} else if (state instanceof TransferDataState.TaskAvailable) {
button.setEnabled(false);
if (((TransferDataState.TaskAvailable) state).isOldTask) {
Toast.makeText(requireContext(),
R.string.removable_drive_ongoing, LENGTH_LONG).show();
}
RemovableDriveTask.State s =
((TransferDataState.TaskAvailable) state).state;
if (s.getTotal() > 0L && progressBar.getVisibility() != VISIBLE) {
progressBar.setVisibility(VISIBLE);
// FIXME if we ever export more than 2 GB, this won't work
progressBar.setMax((int) s.getTotal());
}
progressBar.setProgress((int) s.getDone());
}
}
private void onDocumentCreated(@Nullable Uri uri) {
if (uri == null) return;
viewModel.exportData(uri);
}
}

View File

@@ -8,11 +8,12 @@ import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public interface RemovableDriveModule {
public interface TransferDataModule {
@Binds
@IntoMap
@ViewModelKey(RemovableDriveViewModel.class)
ViewModel bindRemovableDriveViewModel(RemovableDriveViewModel removableDriveViewModel);
ViewModel bindRemovableDriveViewModel(
RemovableDriveViewModel removableDriveViewModel);
}

View File

@@ -0,0 +1,39 @@
package org.briarproject.briar.android.removabledrive;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
@NotNullByDefault
abstract class TransferDataState {
/**
* There is nothing we can send to the chosen contact.
* This only applies to sending data, but not to receiving it.
*/
static class NoDataToSend extends TransferDataState {
}
/**
* We are ready to let the user select a file for sending or receiving data.
*/
static class Ready extends TransferDataState {
}
/**
* A task with state information is available and should be shown in the UI.
*/
static class TaskAvailable extends TransferDataState {
final RemovableDriveTask.State state;
/**
* This is an old task that is still ongoing. The user needs to wait for
* it to complete before starting a new task.
*/
final boolean isOldTask;
TaskAvailable(RemovableDriveTask.State state, boolean isOldTask) {
this.state = state;
this.isOldTask = isOldTask;
}
}
}

View File

@@ -58,7 +58,9 @@ import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.text.HtmlCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -139,6 +141,17 @@ public class UiUtils {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag) {
fm.beginTransaction()
.setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in,
R.anim.step_next_out)
.replace(R.id.fragmentContainer, f, tag)
.addToBackStack(tag)
.commit();
}
public static String getContactDisplayName(Author author,
@Nullable String alias) {
String name = author.getName();

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="m5.9996,0.0002c-0.5304,0 -1.0388,0.2109 -1.4138,0.5859C4.2106,0.9612 3.9998,1.4696 3.9998,2v8.9996c-0.5304,0 -1.0388,0.2109 -1.4138,0.5859 -0.3751,0.3751 -0.5859,0.8845 -0.5859,1.4149L2,23.9998L3.9998,23.9998L3.9998,13.0004L20.0002,13.0004L20.0002,23.9998L22,23.9998L22,13.0004c0,-0.5304 -0.2109,-1.0398 -0.5859,-1.4149 -0.3751,-0.3751 -0.8834,-0.5859 -1.4138,-0.5859L20.0002,2c0,-0.5304 -0.2109,-1.0388 -0.5859,-1.4138C19.0392,0.2111 18.5309,0.0002 18.0004,0.0002ZM5.9996,2L18.0004,2L18.0004,10.9996L5.9996,10.9996ZM8.0004,5.9996L8.0004,8.0004L10.9996,8.0004L10.9996,5.9996ZM13.0004,5.9996v2.0009h2.9991L15.9996,5.9996Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L8,1C6.34,1 5,2.34 5,4v16c0,1.66 1.34,3 3,3h8c1.66,0 3,-1.34 3,-3L19,4c0,-1.66 -1.34,-3 -3,-3zM14,21h-4v-1h4v1zM17.25,18L6.75,18L6.75,4h10.5v14z" />
</vector>

View File

@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="155dp"
android:height="155dp"
android:viewportWidth="155"
android:viewportHeight="155">
<path
android:fillColor="@color/briar_lime_400"
android:pathData="m34.889,27.866c-0.93,-0.021 -1.779,0.612 -1.994,1.555 -0.246,1.077 0.427,2.148 1.504,2.395l5.34,1.223c-6.857,4.771 -12.782,10.788 -17.445,17.768 -5.477,8.197 -9.072,17.492 -10.541,27.219 -0.217,1.437 0.827,2.738 2.271,2.898 1.444,0.16 2.741,-0.88 2.963,-2.316 1.373,-8.889 4.673,-17.381 9.682,-24.877 4.281,-6.406 9.717,-11.929 16.004,-16.316l-1.271,5.559c-0.246,1.077 0.427,2.15 1.504,2.396 1.077,0.246 2.15,-0.427 2.396,-1.504l2.076,-9.08c0.167,-0.732 0.108,-1.458 -0.109,-2.123 -0.033,-0.348 -0.136,-0.693 -0.314,-1.018 -0.354,-0.642 -0.935,-1.078 -1.586,-1.258 -0.31,-0.167 -0.638,-0.31 -0.998,-0.393l-9.078,-2.076c-0.135,-0.031 -0.269,-0.048 -0.402,-0.051zM111.871,30.743c-0.836,0.055 -1.633,0.527 -2.082,1.316 -0.717,1.263 -0.271,2.864 0.961,3.635 8.143,5.096 14.998,12.029 20.002,20.258 4.936,8.117 7.92,17.253 8.746,26.689l-4.25,-3.395c-0.863,-0.689 -2.121,-0.549 -2.811,0.314 -0.689,0.863 -0.549,2.121 0.314,2.811l7.277,5.813c1.726,1.379 4.242,1.097 5.621,-0.629l5.813,-7.277c0.69,-0.863 0.549,-2.121 -0.314,-2.811 -0.863,-0.689 -2.122,-0.549 -2.811,0.314l-3.59,4.494c-0.886,-10.274 -4.128,-20.221 -9.502,-29.057 -5.469,-8.994 -12.974,-16.564 -21.895,-22.105 -0.463,-0.288 -0.979,-0.404 -1.48,-0.371zM45.191,140.86c-0.852,0.028 -1.671,0.446 -2.164,1.209 -0.789,1.219 -0.443,2.855 0.807,3.596 9.034,5.356 19.211,8.518 29.715,9.217 10.503,0.699 21.012,-1.086 30.676,-5.197 1.337,-0.569 1.897,-2.143 1.277,-3.457 -0.621,-1.313 -2.188,-1.87 -3.527,-1.305 -8.852,3.73 -18.466,5.348 -28.076,4.709 -9.61,-0.64 -18.923,-3.518 -27.203,-8.389 -0.47,-0.276 -0.993,-0.4 -1.504,-0.383z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M68.475,3.03C66.724,3.03 65.046,3.726 63.809,4.964 62.571,6.201 61.875,7.879 61.875,9.63L61.875,49.229c0,1.75 0.696,3.43 1.934,4.668 1.238,1.238 2.916,1.934 4.666,1.934h19.801c1.75,0 3.428,-0.696 4.666,-1.934 1.238,-1.238 1.934,-2.918 1.934,-4.668L94.875,9.63c0,-1.75 -0.696,-3.428 -1.934,-4.666C91.704,3.726 90.026,3.03 88.275,3.03ZM68.475,6.331h19.801c0.875,0 1.715,0.346 2.334,0.965 0.619,0.619 0.965,1.459 0.965,2.334L91.574,49.229c0,0.875 -0.346,1.715 -0.965,2.334C89.99,52.182 89.151,52.53 88.275,52.53L68.475,52.53c-0.875,0 -1.715,-0.348 -2.334,-0.967 -0.619,-0.619 -0.965,-1.459 -0.965,-2.334L65.176,9.63c0,-0.875 0.346,-1.715 0.965,-2.334 0.619,-0.619 1.459,-0.965 2.334,-0.965zM11.25,100.03 L0,111.43v22.799C0,136.319 1.688,138.03 3.75,138.03h22.5c2.063,0 3.75,-1.711 3.75,-3.801L30,103.831C30,101.741 28.313,100.03 26.25,100.03ZM131,103.63c-0.796,0 -1.559,0.316 -2.121,0.879 -0.563,0.562 -0.879,1.325 -0.879,2.121v13.5c-0.796,0 -1.559,0.316 -2.121,0.879 -0.563,0.562 -0.879,1.325 -0.879,2.121v16.5h3v-16.5h24v16.5h3v-16.5c0,-0.796 -0.316,-1.559 -0.879,-2.121 -0.562,-0.563 -1.325,-0.879 -2.121,-0.879v-13.5c0,-0.796 -0.316,-1.559 -0.879,-2.121 -0.562,-0.563 -1.325,-0.879 -2.121,-0.879zM12.807,103.831L26.25,103.831v30.398L3.75,134.229L3.75,113.007ZM131,106.63h18v13.5L131,120.13ZM9.375,109.53v7.6h3.75L13.125,109.53ZM15,109.53v7.6h3.75L18.75,109.53ZM20.625,109.53v7.6h3.75L24.375,109.53Z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="m78.375,46.2c0.875,0 1.715,-0.348 2.333,-0.967 0.619,-0.619 0.966,-1.458 0.966,-2.333 0,-0.875 -0.348,-1.715 -0.966,-2.333C80.09,39.948 79.25,39.6 78.375,39.6c-0.875,0 -1.715,0.348 -2.333,0.966 -0.619,0.619 -0.967,1.458 -0.967,2.333 0,0.875 0.348,1.715 0.967,2.333 0.619,0.619 1.458,0.967 2.333,0.967z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="m134,109.6h4.5v3H134Z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="m141.5,109.6h4.5v3h-4.5z" />
</vector>

View File

@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.removabledrive.RemovableDriveActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<Button
android:id="@+id/sneaker_write"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:enabled="false"
android:text=""
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:enabled="true" />
<Button
android:id="@+id/sneaker_read"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:enabled="false"
android:text=""
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/sneaker_write"
tools:enabled="true" />
<TextView
android:id="@+id/sneaker_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large"
android:textSize="12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/sneaker_read" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iconView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="32dp"
app:layout_constraintBottom_toTopOf="@+id/titleView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_max="200dp"
tools:ignore="ContentDescription"
tools:srcCompat="@drawable/alerts_and_states_error"
tools:tint="@color/briar_red_500" />
<TextView
android:id="@+id/titleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iconView"
tools:text="@string/removable_drive_error_send_title" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="@string/removable_drive_error_send_text" />
<Button
android:id="@+id/button"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/finish"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="32dp"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/introView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
app:srcCompat="@drawable/ic_transfer_data"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/introView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:text="@string/removable_drive_intro"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/sendButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/sendButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/removable_drive_title_send"
app:layout_constraintBottom_toTopOf="@+id/receiveButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/receiveButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/removable_drive_title_receive"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<ImageView
android:id="@+id/driveImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:padding="24dp"
app:layout_constraintBottom_toBottomOf="@+id/phoneImageView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@+id/arrowImageView"
app:layout_constraintHorizontal_bias="0.75"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/phoneImageView"
app:layout_constraintWidth_max="150dp"
app:srcCompat="@drawable/ic_flash_drive"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/arrowImageView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/driveImageView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/driveImageView"
app:layout_constraintWidth_max="64dp"
app:srcCompat="@drawable/ic_arrow_forward"
app:tint="@color/briar_brand_green"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:indeterminate="true"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/introTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneImageView"
tools:visibility="visible" />
<ImageView
android:id="@+id/phoneImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
app:layout_constraintBottom_toTopOf="@+id/progressBar"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.25"
app:layout_constraintStart_toEndOf="@+id/arrowImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
app:layout_constraintWidth_max="150dp"
app:srcCompat="@drawable/ic_phone_android"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/introTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="8dp"
android:text="@string/removable_drive_receive_intro"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/fileButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
<Button
android:id="@+id/fileButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/removable_drive_receive_button"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<ImageView
android:id="@+id/phoneImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toTopOf="@+id/progressBar"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@+id/arrowImageView"
app:layout_constraintHeight_min="16dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
app:layout_constraintWidth_max="150dp"
app:srcCompat="@drawable/ic_phone_android"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/arrowImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@+id/phoneImageView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/phoneImageView"
app:layout_constraintWidth_max="64dp"
app:srcCompat="@drawable/ic_arrow_forward"
app:tint="@color/briar_brand_green"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/driveImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:padding="24dp"
app:layout_constraintBottom_toBottomOf="@+id/phoneImageView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/arrowImageView"
app:layout_constraintTop_toTopOf="@+id/phoneImageView"
app:layout_constraintWidth_max="150dp"
app:srcCompat="@drawable/ic_flash_drive"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/introTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneImageView"
tools:visibility="visible" />
<TextView
android:id="@+id/introTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="58dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="8dp"
android:text="@string/removable_drive_send_intro"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/fileButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
<Button
android:id="@+id/fileButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/removable_drive_send_button"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -29,6 +29,12 @@
android:title="@string/menu_item_connect_via_bluetooth"
app:showAsAction="never" />
<item
android:id="@+id/action_transfer_data"
android:title="@string/removable_drive_menu_title"
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/action_delete_all_messages"
android:title="@string/delete_all_messages"
@@ -40,9 +46,4 @@
android:title="@string/delete_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_removable_drive_write"
android:title="Transfer via removable drive"
app:showAsAction="never" />
</menu>

View File

@@ -152,6 +152,7 @@
<string name="open">Open</string>
<string name="change">Change</string>
<string name="start">Start</string>
<string name="finish">Finish</string>
<string name="no_data">No data</string>
<string name="ellipsis"></string>
<string name="text_too_long">The entered text is too long</string>
@@ -690,6 +691,29 @@
<!-- Connections Screen -->
<string name="transports_help_text">Briar can connect to your contacts via the Internet, Wi-Fi or Bluetooth.\n\nAll Internet connections go through the Tor network for privacy.\n\nIf a contact can be reached by multiple methods, Briar uses them in parallel.</string>
<!-- Transfer Data via Removable Drives -->
<string name="removable_drive_menu_title">Transfer data</string>
<string name="removable_drive_intro">You can send messages (and other data) using removable storage such as USB flash drives or SD cards.\n\nTransport the removable storage medium to your contact and they can receive the encrypted messages by importing them into Briar by using the receive button below.</string>
<string name="removable_drive_title_send">Send data</string>
<string name="removable_drive_title_receive">Receive data</string>
<string name="removable_drive_send_intro">Press the button below to create a new file that will contain the encrypted Briar messages.\n\nAfter inserting a storage medium, you should be able to find it with your phone\'s file manager and create a new file there.</string>
<string name="removable_drive_send_no_data">There are currently no messages waiting to be send to this contact.\n\nWrite at least one message and try again.</string>
<string name="removable_drive_send_button">Choose file for export</string>
<string name="removable_drive_ongoing">Please wait for ongoing task to complete</string>
<string name="removable_drive_receive_intro">Plug in the storage medium containing encrypted Briar data for you and select the correct file in the file manager by pressing the button below.</string>
<string name="removable_drive_receive_button">Choose file for import</string>
<string name="removable_drive_success_send_title">Export successful</string>
<string name="removable_drive_success_send_text">Data exported successfully to file. You now have 14 days to transport the storage medium to your contact.\n\nRemember to eject the medium from the notification before unplugging it to prevent data loss.</string>
<string name="removable_drive_success_receive_title">Import successful</string>
<string name="removable_drive_success_receive_text">All messages contained in this file have arrived.</string>
<string name="removable_drive_error_send_title">Error exporting data</string>
<string name="removable_drive_error_send_text">There was an error writing data to the file.\n\nEnsure that the storage medium is properly plugged in and try again.\n\nIf the error persists, you can send feedback to inform about the issue.</string>
<string name="removable_drive_error_receive_title">Error importing data</string>
<string name="removable_drive_error_receive_text">The selected file did not contain anything that Briar could recognize.\n\nEnsure that you chose the correct file and that it was not created more than 14 days ago.</string>
<!-- Screenshots -->
<!-- This is a name to be used in screenshots. Feel free to change it to a local name. -->