Persistent media in multi-send.

master
Greyson Parrelli 2019-03-01 10:50:48 -08:00
parent a79df7d815
commit eb1dd58a0b
33 changed files with 359 additions and 187 deletions

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:fromXDelta="-100%"
android:toXDelta="0%" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:fromXDelta="0%"
android:toXDelta="-100%" />
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/transparent_white_40">
<item android:id="@+id/mask">
<shape>
<corners android:radius="1000dp" />
<solid android:color="@color/white" />
</shape>
</item>
<item android:drawable="@drawable/pill" />
</ripple>

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="1000dp" />
<solid android:color="@color/signal_primary" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="1000dp" />
<solid android:color="@color/core_white" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="1000dp" />
<solid android:color="@color/signal_primary" />
</shape>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/mediapicker_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -6,14 +6,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="2dp"
android:layout_marginBottom="2dp">
android:layout_marginBottom="2dp"
android:animateLayoutChanges="true">
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/mediapicker_image_item_thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="centerCrop"/>
android:scaleType="centerCrop"
tools:src="@drawable/empty_inbox_1"/>
<View
android:layout_width="match_parent"
@ -28,7 +30,7 @@
android:layout_gravity="center"
android:longClickable="false"
android:visibility="gone"
tools:visibility="visible">
tools:visibility="gone">
<ImageView
android:layout_width="15dp"
@ -41,20 +43,29 @@
</FrameLayout>
<FrameLayout
android:id="@+id/mediapicker_selected"
<View
android:id="@+id/mediapicker_select_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent_black_90"
android:visibility="gone">
android:background="@color/transparent_black_90" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_check_white_24dp" />
<ImageView
android:id="@+id/mediapicker_select_on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right|end"
android:padding="6dp"
android:src="@drawable/ic_select_on"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/mediapicker_select_off"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right|end"
android:padding="6dp"
android:src="@drawable/ic_select_off"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/mediasend_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/mediasend_count_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginRight="32dp"
android:layout_gravity="bottom|right|end"
android:padding="8dp"
android:gravity="center"
android:orientation="horizontal"
android:background="@drawable/media_count_button_background"
android:elevation="4dp"
tools:parentTag="android.widget.LinearLayout">
<TextView
android:id="@+id/mediasend_count_button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="28dp"
android:paddingLeft="7dp"
android:paddingRight="7dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:gravity="center"
android:background="@drawable/media_count_number_background"
android:textColor="@color/signal_primary"
android:textSize="18sp"
tools:text="3" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:src="@drawable/ic_arrow_right"
android:tint="@color/core_white"/>
</LinearLayout>
</FrameLayout>

View File

@ -15,6 +15,7 @@ import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
@ -88,6 +89,14 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
initToolbar(view.findViewById(R.id.mediapicker_toolbar));
}
@Override
public void onResume() {
super.onResume();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

View File

@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.util.StableIdGenerator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@ -38,11 +39,7 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
this.media = new ArrayList<>();
this.maxSelection = maxSelection;
this.stableIdGenerator = new StableIdGenerator<>();
this.selected = new TreeSet<>((m1, m2) -> {
if (m1.equals(m2)) return 0;
else if (Long.compare(m2.getDate(), m1.getDate()) == 0) return m2.getUri().compareTo(m1.getUri());
else return Long.compare(m2.getDate(), m1.getDate());
});
this.selected = new LinkedHashSet<>();
setHasStableIds(true);
}
@ -97,13 +94,17 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
private final ImageView thumbnail;
private final View playOverlay;
private final View selectedOverlay;
private final View selectOn;
private final View selectOff;
private final View selectOverlay;
ItemViewHolder(@NonNull View itemView) {
super(itemView);
thumbnail = itemView.findViewById(R.id.mediapicker_image_item_thumbnail);
playOverlay = itemView.findViewById(R.id.mediapicker_play_overlay);
selectedOverlay = itemView.findViewById(R.id.mediapicker_selected);
thumbnail = itemView.findViewById(R.id.mediapicker_image_item_thumbnail);
playOverlay = itemView.findViewById(R.id.mediapicker_play_overlay);
selectOn = itemView.findViewById(R.id.mediapicker_select_on);
selectOff = itemView.findViewById(R.id.mediapicker_select_off);
selectOverlay = itemView.findViewById(R.id.mediapicker_select_overlay);
}
void bind(@NonNull Media media, boolean multiSelect, Set<Media> selected, int maxSelection, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
@ -113,10 +114,13 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
.into(thumbnail);
playOverlay.setVisibility(MediaUtil.isVideoType(media.getMimeType()) ? View.VISIBLE : View.GONE);
selectedOverlay.setVisibility(selected.contains(media) ? View.VISIBLE : View.GONE);
if (selected.isEmpty() && !multiSelect) {
itemView.setOnClickListener(v -> eventListener.onMediaChosen(media));
selectOn.setVisibility(View.GONE);
selectOff.setVisibility(View.GONE);
selectOverlay.setVisibility(View.GONE);
if (maxSelection > 1) {
itemView.setOnLongClickListener(v -> {
selected.add(media);
@ -125,11 +129,17 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
});
}
} else if (selected.contains(media)) {
selectOff.setVisibility(View.VISIBLE);
selectOn.setVisibility(View.VISIBLE);
selectOverlay.setVisibility(View.VISIBLE);
itemView.setOnClickListener(v -> {
selected.remove(media);
eventListener.onMediaSelectionChanged(new ArrayList<>(selected));
});
} else {
selectOff.setVisibility(View.VISIBLE);
selectOn.setVisibility(View.GONE);
selectOverlay.setVisibility(View.GONE);
itemView.setOnClickListener(v -> {
if (selected.size() < maxSelection) {
selected.add(media);

View File

@ -50,8 +50,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
private MediaPickerItemAdapter adapter;
private Controller controller;
private GridLayoutManager layoutManager;
private ActionMode actionMode;
private ActionMode.Callback actionModeCallback;
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection) {
Bundle args = new Bundle();
@ -70,11 +68,10 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
actionModeCallback = new ActionModeCallback();
bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
}
@Override
@ -114,6 +111,8 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
}
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
initMediaObserver(viewModel);
}
@Override
@ -125,16 +124,19 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.mediapicker_default, menu);
public void onPrepareOptionsMenu(Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
MenuItem beginSelectionButton = menu.findItem(R.id.mediapicker_menu_add);
beginSelectionButton.setVisible(!viewModel.getCountButtonState().getValue().getVisibility());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.mediapicker_menu_add) {
adapter.setForcedMultiSelect(true);
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
actionMode.setTitle(getResources().getString(R.string.MediaPickerItemFragment_tap_to_select));
viewModel.onMultiSelectStarted();
return true;
}
return false;
@ -148,23 +150,13 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onMediaChosen(@NonNull Media media) {
controller.onMediaSelected(bucketId, Collections.singleton(media));
viewModel.onSelectedMediaChanged(requireContext(), Collections.singletonList(media));
controller.onMediaSelected(bucketId);
}
@Override
public void onMediaSelectionChanged(@NonNull List<Media> selected) {
adapter.notifyDataSetChanged();
if (actionMode == null && !selected.isEmpty()) {
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
actionMode.setTitle(String.valueOf(selected.size()));
} else if (actionMode != null && selected.isEmpty()) {
actionMode.finish();
} else if (actionMode != null) {
actionMode.setTitle(String.valueOf(selected.size()));
}
viewModel.onSelectedMediaChanged(requireContext(), selected);
}
@ -181,6 +173,12 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
}
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
viewModel.getCountButtonState().observe(this, media -> {
requireActivity().invalidateOptionsMenu();
});
}
private void onScreenWidthChanged(int newWidth) {
if (layoutManager != null) {
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_item_width));
@ -193,55 +191,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
return size.x;
}
private class ActionModeCallback implements ActionMode.Callback {
private int statusBarColor;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.mediapicker_multiselect, menu);
if (Build.VERSION.SDK_INT >= 21) {
Window window = requireActivity().getWindow();
statusBarColor = window.getStatusBarColor();
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
if (menuItem.getItemId() == R.id.mediapicker_menu_confirm) {
List<Media> selected = new ArrayList<>(adapter.getSelected());
actionMode.finish();
viewModel.onSelectedMediaChanged(requireContext(), selected);
controller.onMediaSelected(bucketId, selected);
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
adapter.setSelected(Collections.emptySet());
viewModel.onSelectedMediaChanged(requireContext(), Collections.emptyList());
if (Build.VERSION.SDK_INT >= 21) {
requireActivity().getWindow().setStatusBarColor(statusBarColor);
}
}
}
public interface Controller {
void onMediaSelected(@NonNull String bucketId, @NonNull Collection<Media> media);
void onMediaSelected(@NonNull String bucketId);
}
}

View File

@ -6,11 +6,18 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
@ -23,6 +30,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* Encompasses the entire flow of sending media, starting from the selection process to the actual
@ -56,10 +64,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private Recipient recipient;
private String body;
private TransportOption transport;
private MediaSendViewModel viewModel;
private View countButton;
private TextView countButtonText;
/**
* Get an intent to launch the media send flow starting with the picker.
*/
@ -94,28 +104,42 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
setContentView(R.layout.mediapicker_activity);
setContentView(R.layout.mediasend_activity);
setResult(RESULT_CANCELED);
if (savedInstanceState != null) {
return;
}
countButton = findViewById(R.id.mediasend_count_button);
countButtonText = findViewById(R.id.mediasend_count_button_text);
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
body = getIntent().getStringExtra(KEY_BODY);
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
viewModel.setMediaConstraints(transport.isSms() ? MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1))
: MediaConstraints.getPushMediaConstraints());
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
if (!Util.isEmpty(media)) {
navigateToMediaSend(media, body, transport);
viewModel.onSelectedMediaChanged(this, media);
Fragment fragment = MediaSendFragment.newInstance(transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
.commit();
} else {
navigateToFolderPicker(recipient);
MediaPickerFolderFragment fragment = MediaPickerFolderFragment.newInstance(recipient);
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
.commit();
}
initializeCountButtonObserver(transport, dynamicLanguage.getCurrentLocale());
}
@Override
@ -137,41 +161,34 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
public void onFolderSelected(@NonNull MediaFolder folder) {
viewModel.onFolderSelected(folder.getBucketId());
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(),
folder.getTitle(),
transport.isSms() ? MAX_SMS : MAX_PUSH);
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), transport.isSms() ? MAX_SMS :MAX_PUSH);
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
.replace(R.id.mediapicker_fragment_container, fragment, TAG_ITEM_PICKER)
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
.addToBackStack(null)
.commit();
}
@Override
public void onMediaSelected(@NonNull String bucketId, @NonNull Collection<Media> media) {
MediaSendFragment fragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
.replace(R.id.mediapicker_fragment_container, fragment, TAG_SEND)
.addToBackStack(null)
.commit();
public void onMediaSelected(@NonNull String bucketId) {
navigateToMediaSend(transport, dynamicLanguage.getCurrentLocale());
}
@Override
public void onAddMediaClicked(@NonNull String bucketId) {
// TODO: Get actual folder title somehow
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId,
"",
transport.isSms() ? MAX_SMS : MAX_PUSH);
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", transport.isSms() ? MAX_SMS : MAX_PUSH);
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediapicker_fragment_container, folderFragment, TAG_FOLDER_PICKER)
.setCustomAnimations(R.anim.stationary, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
.addToBackStack(null)
.commit();
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediapicker_fragment_container, itemFragment, TAG_ITEM_PICKER)
.setCustomAnimations(R.anim.slide_from_right, R.anim.stationary, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
.addToBackStack(null)
.commit();
}
@ -214,20 +231,29 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
}
private void navigateToMediaSend(List<Media> media, String body, TransportOption transport) {
viewModel.setInitialSelectedMedia(this, media);
private void initializeCountButtonObserver(@NonNull TransportOption transport, @NonNull Locale locale) {
viewModel.getCountButtonState().observe(this, buttonState -> {
if (buttonState == null) return;
MediaSendFragment sendFragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediapicker_fragment_container, sendFragment, TAG_SEND)
.commit();
countButton.setVisibility(buttonState.getVisibility() ? View.VISIBLE : View.GONE);
countButton.setOnClickListener(v -> navigateToMediaSend(transport, locale));
countButtonText.setText(String.valueOf(buttonState.getCount()));
});
}
private void navigateToFolderPicker(@NonNull Recipient recipient) {
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
private void navigateToMediaSend(@NonNull TransportOption transport, @NonNull Locale locale) {
MediaSendFragment fragment = MediaSendFragment.newInstance(transport, locale);
String backstackTag = null;
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {
getSupportFragmentManager().popBackStack(TAG_SEND, FragmentManager.POP_BACK_STACK_INCLUSIVE);
backstackTag = TAG_SEND;
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediapicker_fragment_container, folderFragment, TAG_FOLDER_PICKER)
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
.addToBackStack(backstackTag)
.commit();
}
}

View File

@ -72,7 +72,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private static final String TAG = MediaSendFragment.class.getSimpleName();
private static final String KEY_BODY = "body";
private static final String KEY_TRANSPORT = "transport";
private static final String KEY_LOCALE = "locale";
@ -99,9 +98,8 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private final Rect visibleBounds = new Rect();
public static MediaSendFragment newInstance(@NonNull String body, @NonNull TransportOption transport, @NonNull Locale locale) {
public static MediaSendFragment newInstance(@NonNull TransportOption transport, @NonNull Locale locale) {
Bundle args = new Bundle();
args.putString(KEY_BODY, body);
args.putParcelable(KEY_TRANSPORT, transport);
args.putSerializable(KEY_LOCALE, locale);
@ -134,9 +132,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
locale = (Locale) getArguments().getSerializable(KEY_LOCALE);
initViewModel();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
@Override
@ -181,7 +176,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
captionText.clearFocus();
composeText.requestFocus();
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(requireActivity().getSupportFragmentManager(), locale);
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager(), locale);
fragmentPager.setAdapter(fragmentPagerAdapter);
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
@ -208,7 +203,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
sendButton.setTransport(transportOption);
sendButton.disableTransport(transportOption.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
composeText.append(getArguments().getString(KEY_BODY));
composeText.append(viewModel.getBody());
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
@ -221,13 +216,25 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
@Override
public void onStart() {
super.onStart();
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
viewModel.onImageEditorStarted();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
}
@Override
public void onStop() {
super.onStop();
fragmentPagerAdapter.saveAllState();
viewModel.saveDrawState(fragmentPagerAdapter.getSavedState());
viewModel.onImageEditorEnded();
}
@Override
@ -328,11 +335,13 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
});
viewModel.getBucketId().observe(this, bucketId -> {
if (bucketId == null || !bucketId.isPresent() || sendButton.getSelectedTransport().isSms()) {
if (bucketId == null) return;
if (sendButton.getSelectedTransport().isSms()) {
addButton.setVisibility(View.GONE);
} else {
addButton.setVisibility(View.VISIBLE);
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId.get()));
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId));
}
});
@ -505,6 +514,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
@Override
public void afterTextChanged(Editable s) {
presentCharactersRemaining();
viewModel.onBodyChanged(s);
}
@Override

View File

@ -9,6 +9,7 @@ import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@ -106,6 +107,15 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
return new HashMap<>(savedState);
}
void saveAllState() {
for (MediaSendPageFragment fragment : fragments.values()) {
Object state = fragment.saveState();
if (state != null) {
savedState.put(fragment.getUri(), state);
}
}
}
void restoreState(@NonNull Map<Uri, Object> state) {
savedState.clear();
savedState.putAll(state);

View File

@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
import java.util.HashMap;
@ -31,31 +30,37 @@ class MediaSendViewModel extends ViewModel {
private final MutableLiveData<List<Media>> selectedMedia;
private final MutableLiveData<List<Media>> bucketMedia;
private final MutableLiveData<Integer> position;
private final MutableLiveData<Optional<String>> bucketId;
private final MutableLiveData<String> bucketId;
private final MutableLiveData<List<MediaFolder>> folders;
private final MutableLiveData<CountButtonState> countButtonState;
private final SingleLiveEvent<Error> error;
private final Map<Uri, Object> savedDrawState;
private MediaConstraints mediaConstraints;
private MediaConstraints mediaConstraints;
private CharSequence body;
private CountButtonState.Visibility countButtonVisibility;
private MediaSendViewModel(@NonNull MediaRepository repository) {
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.countButtonState = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
position.setValue(-1);
countButtonState.setValue(new CountButtonState(0, CountButtonState.Visibility.CONDITIONAL));
}
void setMediaConstraints(@NonNull MediaConstraints mediaConstraints) {
this.mediaConstraints = mediaConstraints;
}
void setInitialSelectedMedia(@NonNull Context context, @NonNull List<Media> newMedia) {
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
@ -63,26 +68,48 @@ class MediaSendViewModel extends ViewModel {
error.postValue(Error.ITEM_TOO_LARGE);
}
boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
if (filteredMedia.size() > 0) {
String computedId = Stream.of(filteredMedia)
.skip(1)
.reduce(filteredMedia.get(0).getBucketId().orNull(), (id, m) -> {
if (Util.equals(id, m.getBucketId().orNull())) {
return id;
} else {
return Media.ALL_MEDIA_BUCKET_ID;
}
});
bucketId.postValue(computedId);
} else {
bucketId.postValue(Media.ALL_MEDIA_BUCKET_ID);
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
}
selectedMedia.postValue(filteredMedia);
bucketId.postValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
countButtonState.postValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
});
}
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(context, newMedia, mediaConstraints);
void onMultiSelectStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
}
void onImageEditorStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
selectedMedia.setValue(filteredMedia);
position.setValue(filteredMedia.isEmpty() ? -1 : 0);
void onImageEditorEnded() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onBodyChanged(@NonNull CharSequence body) {
this.body = body;
}
void onFolderSelected(@NonNull String bucketId) {
this.bucketId.setValue(Optional.of(bucketId));
this.bucketId.setValue(bucketId);
bucketMedia.setValue(Collections.emptyList());
}
@ -91,7 +118,7 @@ class MediaSendViewModel extends ViewModel {
}
void onMediaItemRemoved(int position) {
selectedMedia.getValue().remove(position);
getSelectedMediaOrDefault().remove(position);
selectedMedia.setValue(selectedMedia.getValue());
}
@ -110,11 +137,11 @@ class MediaSendViewModel extends ViewModel {
return savedDrawState;
}
LiveData<List<Media>> getSelectedMedia() {
@NonNull LiveData<List<Media>> getSelectedMedia() {
return selectedMedia;
}
LiveData<List<Media>> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
@NonNull LiveData<List<Media>> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
repository.getMediaInBucket(context, bucketId, bucketMedia::postValue);
return bucketMedia;
}
@ -124,11 +151,19 @@ class MediaSendViewModel extends ViewModel {
return folders;
}
@NonNull LiveData<CountButtonState> getCountButtonState() {
return countButtonState;
}
CharSequence getBody() {
return body;
}
LiveData<Integer> getPosition() {
return position;
}
LiveData<Optional<String>> getBucketId() {
LiveData<String> getBucketId() {
return bucketId;
}
@ -136,17 +171,9 @@ class MediaSendViewModel extends ViewModel {
return error;
}
private Optional<String> computeBucketId(@NonNull List<Media> media) {
if (media.isEmpty() || !media.get(0).getBucketId().isPresent()) return Optional.absent();
String candidate = media.get(0).getBucketId().get();
for (int i = 1; i < media.size(); i++) {
if (!Util.equals(candidate, media.get(i).getBucketId().orNull())) {
return Optional.of(Media.ALL_MEDIA_BUCKET_ID);
}
}
return Optional.of(candidate);
private @NonNull List<Media> getSelectedMediaOrDefault() {
return selectedMedia.getValue() == null ? Collections.emptyList()
: selectedMedia.getValue();
}
private @NonNull List<Media> getFilteredMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull MediaConstraints mediaConstraints) {
@ -165,6 +192,33 @@ class MediaSendViewModel extends ViewModel {
ITEM_TOO_LARGE
}
static class CountButtonState {
private final int count;
private final Visibility visibility;
private CountButtonState(int count, @NonNull Visibility visibility) {
this.count = count;
this.visibility = visibility;
}
int getCount() {
return count;
}
boolean getVisibility() {
switch (visibility) {
case FORCED_ON: return true;
case FORCED_OFF: return false;
case CONDITIONAL: return count > 0;
default: return false;
}
}
enum Visibility {
CONDITIONAL, FORCED_ON, FORCED_OFF
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final MediaRepository repository;

View File

@ -147,6 +147,10 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
public void restoreState(@NonNull Object state) {
if (state instanceof ScribbleView.SavedState) {
savedState = (ScribbleView.SavedState) state;
if (scribbleView != null) {
scribbleView.restoreState(savedState);
}
} else {
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
}

View File

@ -33,6 +33,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
@ -87,6 +88,7 @@ public class ScribbleView extends FrameLayout {
glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
.fitCenter()
.into(imageView);
}