diff --git a/res/anim/slide_from_left.xml b/res/anim/slide_from_left.xml new file mode 100644 index 0000000000..8ad83d09ed --- /dev/null +++ b/res/anim/slide_from_left.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/anim/slide_to_left.xml b/res/anim/slide_to_left.xml new file mode 100644 index 0000000000..698746867a --- /dev/null +++ b/res/anim/slide_to_left.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/drawable-hdpi/ic_arrow_right.png b/res/drawable-hdpi/ic_arrow_right.png new file mode 100644 index 0000000000..bbc68bf0f6 Binary files /dev/null and b/res/drawable-hdpi/ic_arrow_right.png differ diff --git a/res/drawable-hdpi/ic_select_off.png b/res/drawable-hdpi/ic_select_off.png new file mode 100644 index 0000000000..644c55711f Binary files /dev/null and b/res/drawable-hdpi/ic_select_off.png differ diff --git a/res/drawable-hdpi/ic_select_on.png b/res/drawable-hdpi/ic_select_on.png new file mode 100644 index 0000000000..e50fcbdea7 Binary files /dev/null and b/res/drawable-hdpi/ic_select_on.png differ diff --git a/res/drawable-mdpi/ic_arrow_right.png b/res/drawable-mdpi/ic_arrow_right.png new file mode 100644 index 0000000000..0e3af28d8e Binary files /dev/null and b/res/drawable-mdpi/ic_arrow_right.png differ diff --git a/res/drawable-mdpi/ic_select_off.png b/res/drawable-mdpi/ic_select_off.png new file mode 100644 index 0000000000..fcb3c6a4ed Binary files /dev/null and b/res/drawable-mdpi/ic_select_off.png differ diff --git a/res/drawable-mdpi/ic_select_on.png b/res/drawable-mdpi/ic_select_on.png new file mode 100644 index 0000000000..464f1fcf67 Binary files /dev/null and b/res/drawable-mdpi/ic_select_on.png differ diff --git a/res/drawable-v21/media_count_button_background.xml b/res/drawable-v21/media_count_button_background.xml new file mode 100644 index 0000000000..a5fdc45a06 --- /dev/null +++ b/res/drawable-v21/media_count_button_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable-xhdpi/ic_arrow_right.png b/res/drawable-xhdpi/ic_arrow_right.png new file mode 100644 index 0000000000..510690366d Binary files /dev/null and b/res/drawable-xhdpi/ic_arrow_right.png differ diff --git a/res/drawable-xhdpi/ic_select_off.png b/res/drawable-xhdpi/ic_select_off.png new file mode 100644 index 0000000000..2368c9e3d6 Binary files /dev/null and b/res/drawable-xhdpi/ic_select_off.png differ diff --git a/res/drawable-xhdpi/ic_select_on.png b/res/drawable-xhdpi/ic_select_on.png new file mode 100644 index 0000000000..9a83575aeb Binary files /dev/null and b/res/drawable-xhdpi/ic_select_on.png differ diff --git a/res/drawable-xxhdpi/ic_arrow_right.png b/res/drawable-xxhdpi/ic_arrow_right.png new file mode 100644 index 0000000000..daa544853a Binary files /dev/null and b/res/drawable-xxhdpi/ic_arrow_right.png differ diff --git a/res/drawable-xxhdpi/ic_select_off.png b/res/drawable-xxhdpi/ic_select_off.png new file mode 100644 index 0000000000..a4bf4e53d9 Binary files /dev/null and b/res/drawable-xxhdpi/ic_select_off.png differ diff --git a/res/drawable-xxhdpi/ic_select_on.png b/res/drawable-xxhdpi/ic_select_on.png new file mode 100644 index 0000000000..51326566f7 Binary files /dev/null and b/res/drawable-xxhdpi/ic_select_on.png differ diff --git a/res/drawable-xxxhdpi/ic_arrow_right.png b/res/drawable-xxxhdpi/ic_arrow_right.png new file mode 100644 index 0000000000..f7ce1009b8 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_arrow_right.png differ diff --git a/res/drawable-xxxhdpi/ic_select_off.png b/res/drawable-xxxhdpi/ic_select_off.png new file mode 100644 index 0000000000..da08bfe9a9 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_select_off.png differ diff --git a/res/drawable-xxxhdpi/ic_select_on.png b/res/drawable-xxxhdpi/ic_select_on.png new file mode 100644 index 0000000000..177ce25072 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_select_on.png differ diff --git a/res/drawable/media_count_button_background.xml b/res/drawable/media_count_button_background.xml new file mode 100644 index 0000000000..be4d965cfc --- /dev/null +++ b/res/drawable/media_count_button_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/res/drawable/media_count_number_background.xml b/res/drawable/media_count_number_background.xml new file mode 100644 index 0000000000..1ffe671559 --- /dev/null +++ b/res/drawable/media_count_number_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/pill.xml b/res/drawable/pill.xml new file mode 100644 index 0000000000..be4d965cfc --- /dev/null +++ b/res/drawable/pill.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/res/layout/mediapicker_activity.xml b/res/layout/mediapicker_activity.xml deleted file mode 100644 index b7068b063a..0000000000 --- a/res/layout/mediapicker_activity.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/layout/mediapicker_media_item.xml b/res/layout/mediapicker_media_item.xml index bc9c28bd19..e5aa0150a8 100644 --- a/res/layout/mediapicker_media_item.xml +++ b/res/layout/mediapicker_media_item.xml @@ -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"> + android:scaleType="centerCrop" + tools:src="@drawable/empty_inbox_1"/> + tools:visibility="gone"> - - + android:background="@color/transparent_black_90" /> - + - + \ No newline at end of file diff --git a/res/layout/mediasend_activity.xml b/res/layout/mediasend_activity.xml new file mode 100644 index 0000000000..f279426812 --- /dev/null +++ b/res/layout/mediasend_activity.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java index 9215cddf5a..08c47a7399 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java index 651c8c4874..c7217b8976 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemAdapter.java @@ -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(); 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 selected, int maxSelection, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { @@ -113,10 +114,13 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter 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 { 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); diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java index e76bc62a03..5927e50a48 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java @@ -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 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 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); + void onMediaSelected(@NonNull String bucketId); } } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 3441cccf5b..115aabbc23 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -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 = 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) { - 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, 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(); } } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index c96ad9f069..dc81b83c1b 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -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 diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java index d85eba9f1f..2283504fb8 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java @@ -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 state) { savedState.clear(); savedState.putAll(state); diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java index 7bf95e6337..9212e1cdb6 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java @@ -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> selectedMedia; private final MutableLiveData> bucketMedia; private final MutableLiveData position; - private final MutableLiveData> bucketId; + private final MutableLiveData bucketId; private final MutableLiveData> folders; + private final MutableLiveData countButtonState; private final SingleLiveEvent error; private final Map 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 newMedia) { + void onSelectedMediaChanged(@NonNull Context context, @NonNull List newMedia) { repository.getPopulatedMedia(context, newMedia, populatedMedia -> { List 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 newMedia) { - List 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)); + } + + void onImageEditorEnded() { + countButtonVisibility = CountButtonState.Visibility.CONDITIONAL; + countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility)); + } - selectedMedia.setValue(filteredMedia); - position.setValue(filteredMedia.isEmpty() ? -1 : 0); + 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> getSelectedMedia() { + @NonNull LiveData> getSelectedMedia() { return selectedMedia; } - LiveData> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) { + @NonNull LiveData> 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 getCountButtonState() { + return countButtonState; + } + + CharSequence getBody() { + return body; + } + LiveData getPosition() { return position; } - LiveData> getBucketId() { + LiveData getBucketId() { return bucketId; } @@ -136,17 +171,9 @@ class MediaSendViewModel extends ViewModel { return error; } - private Optional computeBucketId(@NonNull List 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 getSelectedMediaOrDefault() { + return selectedMedia.getValue() == null ? Collections.emptyList() + : selectedMedia.getValue(); } private @NonNull List getFilteredMedia(@NonNull Context context, @NonNull List 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; diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java index 9c8dd215df..a5b08f8eed 100644 --- a/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java +++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java @@ -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()); } diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java index 2986dc1f8b..a7453e24de 100644 --- a/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java +++ b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java @@ -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); }