mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 11:19:04 +01:00
Transfer Data UI
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user