Implement camera-first capture flow.
This allows you to take a photo, then choose the recipients after. This also makes it so we only upload the attachment once.
This commit is contained in:
parent
4fbb87b5b7
commit
beaa86389d
44 changed files with 1672 additions and 125 deletions
17
res/drawable-v21/camera_send_button_background.xml
Normal file
17
res/drawable-v21/camera_send_button_background.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?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 android:shape="oval">
|
||||
<solid android:color="@color/transparent_black" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval" >
|
||||
<solid android:color="@color/signal_primary"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
17
res/drawable-v21/media_continue_button_background.xml
Normal file
17
res/drawable-v21/media_continue_button_background.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?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 android:shape="oval">
|
||||
<solid android:color="@color/transparent_black" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval" >
|
||||
<solid android:color="@color/signal_primary"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
4
res/drawable/camera_send_button_background.xml
Normal file
4
res/drawable/camera_send_button_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
|
||||
<solid android:color="@color/signal_primary"/>
|
||||
</shape>
|
9
res/drawable/ic_continue_24.xml
Normal file
9
res/drawable/ic_continue_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M2,11.2h17.8c-0.6,-0.4 -1,-0.7 -1,-0.7L13.2,5l1.1,-1.1l7.5,7.5c0.3,0.3 0.3,0.8 0,1.1l-7.5,7.5L13.2,19l5.6,-5.6c0,0 0.4,-0.3 1,-0.7H2V11.2z"/>
|
||||
</vector>
|
4
res/drawable/media_continue_button_background.xml
Normal file
4
res/drawable/media_continue_button_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
|
||||
<solid android:color="@color/signal_primary"/>
|
||||
</shape>
|
36
res/layout/camera_contact_contact_item.xml
Normal file
36
res/layout/camera_contact_contact_item.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/camera_contacts_horizontal_margin"
|
||||
android:paddingEnd="@dimen/camera_contacts_horizontal_margin"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/camera_contact_item_avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.FromTextView
|
||||
android:id="@+id/camera_contact_item_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="12dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
style="@style/Signal.Text.Body"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/camera_contact_item_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:clickable="false"/>
|
||||
|
||||
</LinearLayout>
|
14
res/layout/camera_contact_header_item.xml
Normal file
14
res/layout/camera_contact_header_item.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/camera_contact_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/camera_contacts_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/camera_contacts_horizontal_margin"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
tools:text="@string/CameraContacts_recent_contacts"/>
|
28
res/layout/camera_contact_invite_item.xml
Normal file
28
res/layout/camera_contact_invite_item.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/camera_contacts_horizontal_margin"
|
||||
android:paddingEnd="@dimen/camera_contacts_horizontal_margin"
|
||||
android:paddingTop="30dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:gravity="center"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:text="@string/CameraContacts_cant_find_who_youre_looking_for"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/camera_contact_invite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
style="@style/Button.Borderless"
|
||||
android:text="@string/CameraContacts_invite_a_contact_to_join_signal" />
|
||||
|
||||
</LinearLayout>
|
123
res/layout/camera_contact_selection_fragment.xml
Normal file
123
res/layout/camera_contact_selection_fragment.xml
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="?android:windowBackground">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/camera_contacts_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/conversation_list_toolbar_background"
|
||||
android:elevation="8dp"
|
||||
android:theme="?actionBarStyle"
|
||||
app:title="@string/CameraContacts_select_signal_recipients"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/camera_contacts_empty"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="60dp"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginEnd="50dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/camera_contacts_toolbar">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/no_contacts"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:text="@string/CameraContacts_no_signal_contacts"
|
||||
android:fontFamily="sans-serif-medium"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:text="@string/CameraContacts_you_can_only_use_the_camera_button"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/camera_contacts_invite_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_gravity="center"
|
||||
style="@style/Button.Borderless"
|
||||
android:text="@string/CameraContacts_invite_a_contact_to_join_signal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/camera_contacts_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/camera_contacts_footer_barrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/camera_contacts_toolbar" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="@drawable/compose_divider_background"
|
||||
app:layout_constraintBottom_toTopOf="@id/camera_contacts_footer_barrier" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/camera_contacts_footer_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="camera_contacts_selected_list,camera_contacts_send_button" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/camera_contacts_selected_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingTop="22dp"
|
||||
android:paddingBottom="22dp"
|
||||
android:paddingStart="16dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/camera_contacts_send_button"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/camera_contacts_send_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:src="@drawable/ic_send_push_white_24dp"
|
||||
android:background="@drawable/camera_send_button_background"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/camera_contacts_selected_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/camera_contacts_selected_list" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/camera_contacts_footer_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="camera_contacts_send_button,camera_contacts_selected_list,view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
7
res/layout/camera_contact_selection_item.xml
Normal file
7
res/layout/camera_contact_selection_item.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.FromTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="5sp"
|
||||
style="@style/Signal.Text.Preview"/>
|
|
@ -35,6 +35,7 @@
|
|||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginBottom="42dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toTopOf="@id/camera_capture_button"
|
||||
app:layout_constraintStart_toStartOf="@id/camera_capture_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/camera_capture_button"
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintTop_toTopOf="@id/camera_capture_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/camera_capture_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -66,6 +66,17 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/camera_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="82dp"
|
||||
android:src="@drawable/ic_camera_alt_white_24dp"
|
||||
android:tint="@color/core_grey_60"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/core_white"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
|
|
|
@ -133,6 +133,19 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediasend_continue_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:padding="6dp"
|
||||
android:background="@drawable/media_continue_button_background"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_continue_24"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
|
|
13
res/menu/camera_contacts.xml
Normal file
13
res/menu/camera_contacts.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:title=""
|
||||
android:id="@+id/menu_search"
|
||||
android:icon="@drawable/ic_search_white_24dp"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="collapseActionView|always"/>
|
||||
|
||||
</menu>
|
|
@ -26,6 +26,8 @@
|
|||
<dimen name="album_5_cell_size_big">104dp</dimen>
|
||||
<dimen name="album_5_cell_size_small">69dp</dimen>
|
||||
|
||||
<dimen name="camera_contacts_horizontal_margin">16dp</dimen>
|
||||
|
||||
<dimen name="message_corner_radius">16dp</dimen>
|
||||
<dimen name="message_corner_collapse_radius">4dp</dimen>
|
||||
<dimen name="message_bubble_corner_radius">2dp</dimen>
|
||||
|
|
|
@ -91,6 +91,17 @@
|
|||
<!-- CameraActivity -->
|
||||
<string name="CameraActivity_image_save_failure">Failed to save image.</string>
|
||||
|
||||
<!-- CameraContacts -->
|
||||
<string name="CameraContacts_recent_contacts">Recent contacts</string>
|
||||
<string name="CameraContacts_signal_contacts">Signal contacts</string>
|
||||
<string name="CameraContacts_signal_groups">Signal groups</string>
|
||||
<string name="CameraContacts_you_can_share_with_a_maximum_of_n_conversations">You can share with a maximum of %d conversations.</string>
|
||||
<string name="CameraContacts_select_signal_recipients">Select Signal recipients</string>
|
||||
<string name="CameraContacts_no_signal_contacts">No Signal contacts</string>
|
||||
<string name="CameraContacts_you_can_only_use_the_camera_button">You can only use the camera button to send photos to Signal contacts. </string>
|
||||
<string name="CameraContacts_cant_find_who_youre_looking_for">Can\'t find who you\'re looking for?</string>
|
||||
<string name="CameraContacts_invite_a_contact_to_join_signal">Invite a contact to join Signal</string>
|
||||
|
||||
<!-- ClearProfileActivity -->
|
||||
<string name="ClearProfileActivity_remove">Remove</string>
|
||||
<string name="ClearProfileActivity_remove_profile_photo">Remove profile photo?</string>
|
||||
|
@ -480,6 +491,8 @@
|
|||
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">An item was removed because it exceeded the size limit</string>
|
||||
<string name="MediaSendActivity_camera_unavailable">Camera unavailable.</string>
|
||||
<string name="MediaSendActivity_message_to_s">Message to %s</string>
|
||||
<string name="MediaSendActivity_message">Message</string>
|
||||
<string name="MediaSendActivity_select_recipients">Select recipients</string>
|
||||
<plurals name="MediaSendActivity_cant_share_more_than_n_items">
|
||||
<item quantity="one">You can\'t share more than %d item.</item>
|
||||
<item quantity="other">You can\'t share more than %d items.</item>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
|
@ -32,6 +33,8 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
@ -51,6 +54,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
|
@ -74,9 +78,11 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
|||
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
@ -111,6 +117,7 @@ public class ConversationListFragment extends Fragment
|
|||
private ImageView emptyImage;
|
||||
private TextView emptySearch;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private FloatingActionButton cameraFab;
|
||||
private Locale locale;
|
||||
private String queryFilter = "";
|
||||
private boolean archive;
|
||||
|
@ -129,6 +136,7 @@ public class ConversationListFragment extends Fragment
|
|||
reminderView = ViewUtil.findById(view, R.id.reminder);
|
||||
list = ViewUtil.findById(view, R.id.list);
|
||||
fab = ViewUtil.findById(view, R.id.fab);
|
||||
cameraFab = ViewUtil.findById(view, R.id.camera_fab);
|
||||
emptyState = ViewUtil.findById(view, R.id.empty_state);
|
||||
emptyImage = ViewUtil.findById(view, R.id.empty);
|
||||
emptySearch = ViewUtil.findById(view, R.id.empty_search);
|
||||
|
@ -153,6 +161,16 @@ public class ConversationListFragment extends Fragment
|
|||
|
||||
setHasOptionsMenu(true);
|
||||
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
|
||||
cameraFab.setOnClickListener(v -> {
|
||||
Permissions.with(requireActivity())
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> startActivity(MediaSendActivity.buildCameraFirstIntent(requireActivity())))
|
||||
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
});
|
||||
initializeListAdapter();
|
||||
initializeTypingObserver();
|
||||
}
|
||||
|
|
|
@ -106,6 +106,16 @@ public class TransportOptions {
|
|||
throw new AssertionError("No options of default type!");
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
|
||||
return new TransportOption(Type.TEXTSECURE,
|
||||
R.drawable.ic_send_push_white_24dp,
|
||||
context.getResources().getColor(R.color.textsecure_primary),
|
||||
context.getString(R.string.ConversationActivity_transport_signal),
|
||||
context.getString(R.string.conversation_activity__type_message_push),
|
||||
new PushCharacterCalculator());
|
||||
|
||||
}
|
||||
|
||||
private @Nullable TransportOption findEnabledSmsTransportOption(Optional<Integer> subscriptionId) {
|
||||
if (subscriptionId.isPresent()) {
|
||||
final int subId = subscriptionId.get();
|
||||
|
@ -157,11 +167,7 @@ public class TransportOptions {
|
|||
new SmsCharacterCalculator()));
|
||||
}
|
||||
|
||||
results.add(new TransportOption(Type.TEXTSECURE, R.drawable.ic_send_push_white_24dp,
|
||||
context.getResources().getColor(R.color.textsecure_primary),
|
||||
context.getString(R.string.ConversationActivity_transport_signal),
|
||||
context.getString(R.string.conversation_activity__type_message_push),
|
||||
new PushCharacterCalculator()));
|
||||
results.add(getPushTransportOption(context));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.components;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
|
@ -36,6 +39,10 @@ public class FromTextView extends EmojiTextView {
|
|||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read) {
|
||||
setText(recipient, read, null);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
String fromString = recipient.toShortString();
|
||||
|
||||
int typeface;
|
||||
|
@ -72,6 +79,10 @@ public class FromTextView extends EmojiTextView {
|
|||
builder.append(fromSpan);
|
||||
}
|
||||
|
||||
if (suffix != null) {
|
||||
builder.append(suffix);
|
||||
}
|
||||
|
||||
setText(builder);
|
||||
|
||||
if (recipient.isBlocked()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
|
|
|
@ -232,7 +232,7 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, MMS_ID + " = ?", new String[] {mmsId+""},
|
||||
null, null, null);
|
||||
null, null, UNIQUE_ID + " ASC, " + ROW_ID + " ASC");
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.addAll(getAttachment(cursor));
|
||||
|
@ -420,6 +420,44 @@ public class AttachmentDatabase extends Database {
|
|||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
|
||||
}
|
||||
|
||||
public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull AttachmentId destinationId)
|
||||
throws MmsException, IOException
|
||||
{
|
||||
DatabaseAttachment sourceAttachment = getAttachment(sourceId);
|
||||
|
||||
if (sourceAttachment == null) {
|
||||
throw new MmsException("Cannot find attachment for source!");
|
||||
}
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
DataInfo copyToDataInfo = getAttachmentDataFileInfo(destinationId, DATA);
|
||||
|
||||
if (copyToDataInfo == null) {
|
||||
throw new MmsException("No attachment data found for destination!");
|
||||
}
|
||||
|
||||
copyToDataInfo = setAttachmentData(copyToDataInfo.file, getAttachmentStream(sourceId, 0));
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
contentValues.put(SIZE, copyToDataInfo.length);
|
||||
contentValues.put(DATA_RANDOM, copyToDataInfo.random);
|
||||
|
||||
contentValues.put(TRANSFER_STATE, sourceAttachment.getTransferState());
|
||||
contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation());
|
||||
contentValues.put(DIGEST, sourceAttachment.getDigest());
|
||||
contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey());
|
||||
contentValues.put(NAME, sourceAttachment.getRelay());
|
||||
contentValues.put(SIZE, sourceAttachment.getSize());
|
||||
contentValues.put(FAST_PREFLIGHT_ID, sourceAttachment.getFastPreflightId());
|
||||
contentValues.put(WIDTH, sourceAttachment.getWidth());
|
||||
contentValues.put(HEIGHT, sourceAttachment.getHeight());
|
||||
contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType());
|
||||
|
||||
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings());
|
||||
}
|
||||
|
||||
public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attachment attachment) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
|
|
@ -43,7 +43,7 @@ public class RecipientDatabase extends Database {
|
|||
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
|
||||
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
|
||||
private static final String EXPIRE_MESSAGES = "expire_messages";
|
||||
private static final String REGISTERED = "registered";
|
||||
static final String REGISTERED = "registered";
|
||||
private static final String PROFILE_KEY = "profile_key";
|
||||
private static final String SYSTEM_DISPLAY_NAME = "system_display_name";
|
||||
private static final String SYSTEM_PHOTO_URI = "system_contact_photo";
|
||||
|
|
|
@ -377,6 +377,14 @@ public class ThreadDatabase extends Database {
|
|||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public Cursor getRecentPushConversationList(int limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = MESSAGE_COUNT + " != 0 AND (" + RecipientDatabase.REGISTERED + " = " + RecipientDatabase.RegisteredState.REGISTERED.getId() + " OR " + GroupDatabase.GROUP_ID + " NOT NULL)";
|
||||
String query = createQuery(where, limit);
|
||||
|
||||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public Cursor getConversationList() {
|
||||
return getConversationList("0");
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
return then(Collections.singletonList(job));
|
||||
}
|
||||
|
||||
public Chain then(@NonNull List<Job> jobs) {
|
||||
public Chain then(@NonNull List<? extends Job> jobs) {
|
||||
if (!jobs.isEmpty()) {
|
||||
this.jobs.add(new ArrayList<>(jobs));
|
||||
}
|
||||
|
|
108
src/org/thoughtcrime/securesms/jobs/AttachmentCopyJob.java
Normal file
108
src/org/thoughtcrime/securesms/jobs/AttachmentCopyJob.java
Normal file
|
@ -0,0 +1,108 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Copies the data from one attachment to another. Useful when you only want to send an attachment
|
||||
* once, and then copy the data from that upload to other messages.
|
||||
*/
|
||||
public class AttachmentCopyJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "AttachmentCopyJob";
|
||||
|
||||
private static final String KEY_SOURCE_ID = "source_id";
|
||||
private static final String KEY_DESTINATION_IDS = "destination_ids";
|
||||
|
||||
private final AttachmentId sourceId;
|
||||
private final List<AttachmentId> destinationIds;
|
||||
|
||||
public AttachmentCopyJob(@NonNull AttachmentId sourceId, @NonNull List<AttachmentId> destinationIds) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("AttachmentCopyJob")
|
||||
.setMaxAttempts(3)
|
||||
.build(),
|
||||
sourceId,
|
||||
destinationIds);
|
||||
}
|
||||
|
||||
private AttachmentCopyJob(@NonNull Parameters parameters,
|
||||
@NonNull AttachmentId sourceId,
|
||||
@NonNull List<AttachmentId> destinationIds)
|
||||
{
|
||||
super(parameters);
|
||||
this.sourceId = sourceId;
|
||||
this.destinationIds = destinationIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
try {
|
||||
String sourceIdString = JsonUtils.toJson(sourceId);
|
||||
String[] destinationIdStrings = new String[destinationIds.size()];
|
||||
|
||||
for (int i = 0; i < destinationIds.size(); i++) {
|
||||
destinationIdStrings[i] = JsonUtils.toJson(destinationIds.get(i));
|
||||
}
|
||||
|
||||
return new Data.Builder().putString(KEY_SOURCE_ID, sourceIdString)
|
||||
.putStringArray(KEY_DESTINATION_IDS, destinationIdStrings)
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
for (AttachmentId destinationId : destinationIds) {
|
||||
database.copyAttachmentData(sourceId, destinationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() { }
|
||||
|
||||
public static final class Factory implements Job.Factory<AttachmentCopyJob> {
|
||||
@Override
|
||||
public @NonNull AttachmentCopyJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
String sourceIdStrings = data.getString(KEY_SOURCE_ID);
|
||||
String[] destinationIdStrings = data.getStringArray(KEY_DESTINATION_IDS);
|
||||
|
||||
AttachmentId sourceId = JsonUtils.fromJson(sourceIdStrings, AttachmentId.class);
|
||||
List<AttachmentId> destinationIds = new ArrayList<>(destinationIdStrings.length);
|
||||
|
||||
for (String idString : destinationIdStrings) {
|
||||
destinationIds.add(JsonUtils.fromJson(idString, AttachmentId.class));
|
||||
}
|
||||
|
||||
return new AttachmentCopyJob(parameters, sourceId, destinationIds);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ public final class JobManagerFactories {
|
|||
|
||||
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
||||
return new HashMap<String, Job.Factory>() {{
|
||||
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
|
|
|
@ -39,7 +39,6 @@ import com.bumptech.glide.request.transition.Transition;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
@ -267,7 +266,7 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
|||
});
|
||||
|
||||
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
|
||||
|
||||
viewModel.onCameraControlsInitialized();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.adapter.SectionedRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class CameraContactAdapter extends SectionedRecyclerViewAdapter<String, CameraContactAdapter.ContactSection> {
|
||||
|
||||
private static final int TYPE_INVITE = 1337;
|
||||
private static final long ID_INVITE = Long.MAX_VALUE;
|
||||
|
||||
private static final String TAG_RECENT = "recent";
|
||||
private static final String TAG_ALL = "all";
|
||||
private static final String TAG_GROUPS = "groups";
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<Recipient> selected;
|
||||
private final CameraContactListener cameraContactListener;
|
||||
|
||||
|
||||
private final List<ContactSection> sections = new ArrayList<ContactSection>(3) {{
|
||||
ContactSection recentContacts = new ContactSection(TAG_RECENT, R.string.CameraContacts_recent_contacts, Collections.emptyList(), 0);
|
||||
ContactSection allContacts = new ContactSection(TAG_ALL, R.string.CameraContacts_signal_contacts, Collections.emptyList(), recentContacts.size());
|
||||
ContactSection groups = new ContactSection(TAG_GROUPS, R.string.CameraContacts_signal_groups, Collections.emptyList(), recentContacts.size() + allContacts.size());
|
||||
|
||||
add(recentContacts);
|
||||
add(allContacts);
|
||||
add(groups);
|
||||
}};
|
||||
|
||||
CameraContactAdapter(@NonNull GlideRequests glideRequests, @NonNull CameraContactListener listener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.selected = new HashSet<>();
|
||||
this.cameraContactListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull List<ContactSection> getSections() {
|
||||
return sections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int globalPosition) {
|
||||
if (isInvitePosition(globalPosition)) {
|
||||
return ID_INVITE;
|
||||
} else {
|
||||
return super.getItemId(globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int globalPosition) {
|
||||
if (isInvitePosition(globalPosition)) {
|
||||
return TYPE_INVITE;
|
||||
} else {
|
||||
return super.getItemViewType(globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||
if (viewType == TYPE_INVITE) {
|
||||
return new InviteViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.camera_contact_invite_item, viewGroup, false));
|
||||
} else {
|
||||
return super.onCreateViewHolder(viewGroup, viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull RecyclerView.ViewHolder createHeaderViewHolder(@NonNull ViewGroup parent) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.camera_contact_header_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull RecyclerView.ViewHolder createContentViewHolder(@NonNull ViewGroup parent) {
|
||||
return new ContactViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.camera_contact_contact_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable RecyclerView.ViewHolder createEmptyViewHolder(@NonNull ViewGroup viewGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int globalPosition) {
|
||||
if (isInvitePosition(globalPosition)) {
|
||||
((InviteViewHolder) holder).bind(cameraContactListener);
|
||||
} else {
|
||||
super.onBindViewHolder(holder, globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindViewHolder(@NonNull RecyclerView.ViewHolder holder, @NonNull ContactSection section, int localPosition) {
|
||||
section.bind(holder, localPosition, selected, glideRequests, cameraContactListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = super.getItemCount();
|
||||
return count > 0 ? count + 1 : 0;
|
||||
}
|
||||
|
||||
public void setContacts(@NonNull CameraContacts contacts, @NonNull Collection<Recipient> selected) {
|
||||
ContactSection recentContacts = new ContactSection(TAG_RECENT, R.string.CameraContacts_recent_contacts, contacts.getRecents(), 0);
|
||||
ContactSection allContacts = new ContactSection(TAG_ALL, R.string.CameraContacts_signal_contacts, contacts.getContacts(), recentContacts.size());
|
||||
ContactSection groups = new ContactSection(TAG_GROUPS, R.string.CameraContacts_signal_groups, contacts.getGroups(), recentContacts.size() + allContacts.size());
|
||||
|
||||
sections.clear();
|
||||
sections.add(recentContacts);
|
||||
sections.add(allContacts);
|
||||
sections.add(groups);
|
||||
|
||||
this.selected.clear();
|
||||
this.selected.addAll(selected);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private boolean isInvitePosition(int globalPosition) {
|
||||
int count = getItemCount();
|
||||
return count > 0 && globalPosition == getItemCount() - 1;
|
||||
}
|
||||
|
||||
public static class ContactSection extends SectionedRecyclerViewAdapter.Section<String> {
|
||||
|
||||
private final String tag;
|
||||
private final int titleResId;
|
||||
private final List<Recipient> recipients;
|
||||
|
||||
public ContactSection(@NonNull String tag, @StringRes int titleResId, @NonNull List<Recipient> recipients, int offset) {
|
||||
super(offset);
|
||||
this.tag = tag;
|
||||
this.titleResId = titleResId;
|
||||
this.recipients = recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasEmptyState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentSize() {
|
||||
return recipients.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(@NonNull StableIdGenerator<String> idGenerator, int globalPosition) {
|
||||
int localPosition = getLocalPosition(globalPosition);
|
||||
|
||||
if (localPosition == 0) {
|
||||
return idGenerator.getId(tag);
|
||||
} else {
|
||||
return idGenerator.getId(recipients.get(localPosition - 1).getAddress().serialize());
|
||||
}
|
||||
}
|
||||
|
||||
void bind(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
int localPosition,
|
||||
@NonNull Set<Recipient> selected,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull CameraContactListener cameraContactListener)
|
||||
{
|
||||
if (localPosition == 0) {
|
||||
((HeaderViewHolder) viewHolder).bind(titleResId);
|
||||
} else {
|
||||
Recipient recipient = recipients.get(localPosition - 1);
|
||||
((ContactViewHolder) viewHolder).bind(recipient, selected.contains(recipient), glideRequests, cameraContactListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView title;
|
||||
|
||||
HeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.title = itemView.findViewById(R.id.camera_contact_header);
|
||||
}
|
||||
|
||||
void bind(@StringRes int titleResId) {
|
||||
this.title.setText(titleResId);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ContactViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final FromTextView name;
|
||||
private final CheckBox checkbox;
|
||||
|
||||
ContactViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.avatar = itemView.findViewById(R.id.camera_contact_item_avatar);
|
||||
this.name = itemView.findViewById(R.id.camera_contact_item_name);
|
||||
this.checkbox = itemView.findViewById(R.id.camera_contact_item_checkbox);
|
||||
}
|
||||
|
||||
void bind(@NonNull Recipient recipient,
|
||||
boolean selected,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull CameraContactListener listener)
|
||||
{
|
||||
avatar.setAvatar(glideRequests, recipient, false);
|
||||
name.setText(recipient);
|
||||
itemView.setOnClickListener(v -> listener.onContactClicked(recipient));
|
||||
checkbox.setChecked(selected);
|
||||
}
|
||||
}
|
||||
|
||||
private static class InviteViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final View inviteButton;
|
||||
|
||||
public InviteViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
inviteButton = itemView.findViewById(R.id.camera_contact_invite);
|
||||
}
|
||||
|
||||
void bind(@NonNull CameraContactListener listener) {
|
||||
inviteButton.setOnClickListener(v -> listener.onInviteContactsClicked());
|
||||
}
|
||||
}
|
||||
|
||||
interface CameraContactListener {
|
||||
void onContactClicked(@NonNull Recipient recipient);
|
||||
void onInviteContactsClicked();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class CameraContactSelectionAdapter extends RecyclerView.Adapter<CameraContactSelectionAdapter.RecipientViewHolder> {
|
||||
|
||||
private final List<Recipient> recipients = new ArrayList<>();
|
||||
private final StableIdGenerator<String> idGenerator = new StableIdGenerator<>();
|
||||
|
||||
CameraContactSelectionAdapter() {
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return idGenerator.getId(recipients.get(position).getAddress().serialize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RecipientViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new RecipientViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.camera_contact_selection_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecipientViewHolder holder, int position) {
|
||||
holder.bind(recipients.get(position), position == recipients.size() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return recipients.size();
|
||||
}
|
||||
|
||||
void setRecipients(@NonNull List<Recipient> recipients) {
|
||||
this.recipients.clear();
|
||||
this.recipients.addAll(recipients);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class RecipientViewHolder extends MediaDocumentsAdapter.ViewHolder {
|
||||
|
||||
private final FromTextView name;
|
||||
|
||||
RecipientViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
name = (FromTextView) itemView;
|
||||
}
|
||||
|
||||
void bind(@NonNull Recipient recipient, boolean isLast) {
|
||||
name.setText(recipient, true, isLast ? null : ",");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.InviteActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Fragment that selects Signal contacts. Intended to be used in the camera-first capture flow.
|
||||
*/
|
||||
public class CameraContactSelectionFragment extends Fragment implements CameraContactAdapter.CameraContactListener {
|
||||
|
||||
private Controller controller;
|
||||
private MediaSendViewModel mediaSendViewModel;
|
||||
private CameraContactSelectionViewModel contactViewModel;
|
||||
private RecyclerView contactList;
|
||||
private CameraContactAdapter contactAdapter;
|
||||
private RecyclerView selectionList;
|
||||
private CameraContactSelectionAdapter selectionAdapter;
|
||||
private Toolbar toolbar;
|
||||
private View sendButton;
|
||||
private Group selectionFooterGroup;
|
||||
private ViewGroup cameraContactsEmpty;
|
||||
private View inviteButton;
|
||||
|
||||
public static Fragment newInstance() {
|
||||
return new CameraContactSelectionFragment();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
this.mediaSendViewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
this.contactViewModel = ViewModelProviders.of(requireActivity(), new CameraContactSelectionViewModel.Factory(new CameraContactsRepository(requireContext())))
|
||||
.get(CameraContactSelectionViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller interface.");
|
||||
}
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
int theme = TextSecurePreferences.getTheme(inflater.getContext()).equals("light") ? R.style.TextSecure_LightTheme
|
||||
: R.style.TextSecure_DarkTheme;
|
||||
return ThemeUtil.getThemedInflater(inflater.getContext(), inflater, theme)
|
||||
.inflate(R.layout.camera_contact_selection_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
this.contactList = view.findViewById(R.id.camera_contacts_list);
|
||||
this.selectionList = view.findViewById(R.id.camera_contacts_selected_list);
|
||||
this.toolbar = view.findViewById(R.id.camera_contacts_toolbar);
|
||||
this.sendButton = view.findViewById(R.id.camera_contacts_send_button);
|
||||
this.selectionFooterGroup = view.findViewById(R.id.camera_contacts_footer_group);
|
||||
this.cameraContactsEmpty = view.findViewById(R.id.camera_contacts_empty);
|
||||
this.inviteButton = view.findViewById(R.id.camera_contacts_invite_button);
|
||||
this.contactAdapter = new CameraContactAdapter(GlideApp.with(this), this);
|
||||
this.selectionAdapter = new CameraContactSelectionAdapter();
|
||||
|
||||
contactList.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
contactList.setAdapter(contactAdapter);
|
||||
|
||||
selectionList.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
selectionList.setAdapter(selectionAdapter);
|
||||
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
|
||||
inviteButton.setOnClickListener(v -> onInviteContactsClicked());
|
||||
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mediaSendViewModel.onContactSelectStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
requireActivity().getMenuInflater().inflate(R.menu.camera_contacts, menu);
|
||||
|
||||
MenuItem searchViewItem = menu.findItem(R.id.menu_search);
|
||||
SearchView searchView = (SearchView) searchViewItem.getActionView();
|
||||
SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
contactViewModel.onQueryUpdated(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String query) {
|
||||
contactViewModel.onQueryUpdated(query);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
searchViewItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||
searchView.setOnQueryTextListener(queryListener);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
searchView.setOnQueryTextListener(null);
|
||||
contactViewModel.onSearchClosed();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactClicked(@NonNull Recipient recipient) {
|
||||
contactViewModel.onContactClicked(recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInviteContactsClicked() {
|
||||
startActivity(new Intent(requireContext(), InviteActivity.class));
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
contactViewModel.getContacts().observe(getViewLifecycleOwner(), contactState -> {
|
||||
if (contactState == null) return;
|
||||
|
||||
if (contactState.getContacts().isEmpty()) {
|
||||
cameraContactsEmpty.setVisibility(View.VISIBLE);
|
||||
selectionFooterGroup.setVisibility(View.GONE);
|
||||
} else {
|
||||
cameraContactsEmpty.setVisibility(View.GONE);
|
||||
|
||||
sendButton.setOnClickListener(v -> controller.onCameraContactsSendClicked(contactState.getSelected()));
|
||||
|
||||
contactAdapter.setContacts(contactState.getContacts(), contactState.getSelected());
|
||||
selectionAdapter.setRecipients(contactState.getSelected());
|
||||
|
||||
selectionFooterGroup.setVisibility(contactState.getSelected().isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
contactViewModel.getError().observe(getViewLifecycleOwner(), error -> {
|
||||
if (error == null) return;
|
||||
|
||||
if (error == CameraContactSelectionViewModel.Error.MAX_SELECTION) {
|
||||
String message = getString(R.string.CameraContacts_you_can_share_with_a_maximum_of_n_conversations, CameraContactSelectionViewModel.MAX_SELECTION_COUNT);
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onCameraContactsSendClicked(@NonNull List<Recipient> recipients);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class CameraContactSelectionViewModel extends ViewModel {
|
||||
|
||||
static final int MAX_SELECTION_COUNT = 16;
|
||||
|
||||
private final CameraContactsRepository repository;
|
||||
private final MutableLiveData<ContactState> contacts;
|
||||
private final SingleLiveEvent<Error> error;
|
||||
private final Set<Recipient> selected;
|
||||
|
||||
private CameraContactSelectionViewModel(@NonNull CameraContactsRepository repository) {
|
||||
this.repository = repository;
|
||||
this.contacts = new MutableLiveData<>();
|
||||
this.error = new SingleLiveEvent<>();
|
||||
this.selected = new LinkedHashSet<>();
|
||||
|
||||
repository.getCameraContacts(cameraContacts -> {
|
||||
Util.runOnMain(() -> {
|
||||
contacts.postValue(new ContactState(cameraContacts, new ArrayList<>(selected)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<ContactState> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
LiveData<Error> getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
void onSearchClosed() {
|
||||
onQueryUpdated("");
|
||||
}
|
||||
|
||||
void onQueryUpdated(String query) {
|
||||
repository.getCameraContacts(query, cameraContacts -> {
|
||||
Util.runOnMain(() -> {
|
||||
contacts.postValue(new ContactState(cameraContacts, new ArrayList<>(selected)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void onRefresh() {
|
||||
repository.getCameraContacts(cameraContacts -> {
|
||||
Util.runOnMain(() -> {
|
||||
contacts.postValue(new ContactState(cameraContacts, new ArrayList<>(selected)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void onContactClicked(@NonNull Recipient recipient) {
|
||||
if (selected.contains(recipient)) {
|
||||
selected.remove(recipient);
|
||||
} else if (selected.size() < MAX_SELECTION_COUNT) {
|
||||
selected.add(recipient);
|
||||
} else {
|
||||
error.postValue(Error.MAX_SELECTION);
|
||||
}
|
||||
|
||||
ContactState currentState = contacts.getValue();
|
||||
|
||||
if (currentState != null) {
|
||||
contacts.setValue(new ContactState(currentState.getContacts(), new ArrayList<>(selected)));
|
||||
}
|
||||
}
|
||||
|
||||
static class ContactState {
|
||||
private final CameraContacts contacts;
|
||||
private final List<Recipient> selected;
|
||||
|
||||
ContactState(CameraContacts contacts, List<Recipient> selected) {
|
||||
this.contacts = contacts;
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public CameraContacts getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public List<Recipient> getSelected() {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
enum Error {
|
||||
MAX_SELECTION
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final CameraContactsRepository repository;
|
||||
|
||||
Factory(CameraContactsRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new CameraContactSelectionViewModel(repository));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
39
src/org/thoughtcrime/securesms/mediasend/CameraContacts.java
Normal file
39
src/org/thoughtcrime/securesms/mediasend/CameraContacts.java
Normal file
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the list of results to display in the {@link CameraContactSelectionFragment}.
|
||||
*/
|
||||
public class CameraContacts {
|
||||
|
||||
private final List<Recipient> recents;
|
||||
private final List<Recipient> contacts;
|
||||
private final List<Recipient> groups;
|
||||
|
||||
public CameraContacts(@NonNull List<Recipient> recents, @NonNull List<Recipient> contacts, @NonNull List<Recipient> groups) {
|
||||
this.recents = recents;
|
||||
this.contacts = contacts;
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getRecents() {
|
||||
return recents;
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return recents.isEmpty() && contacts.isEmpty() && groups.isEmpty();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.net.CookieHandler;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles retrieving the data to be shown in {@link CameraContactSelectionFragment}.
|
||||
*/
|
||||
class CameraContactsRepository {
|
||||
|
||||
private static final int RECENT_MAX = 7;
|
||||
|
||||
private final Context context;
|
||||
private final ThreadDatabase threadDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final ContactsDatabase contactsDatabase;
|
||||
|
||||
CameraContactsRepository(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.contactsDatabase = DatabaseFactory.getContactsDatabase(context);
|
||||
}
|
||||
|
||||
void getCameraContacts(@NonNull Callback<CameraContacts> callback) {
|
||||
getCameraContacts("", callback);
|
||||
}
|
||||
|
||||
void getCameraContacts(@NonNull String query, @NonNull Callback<CameraContacts> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<Recipient> recents = getRecents(query);
|
||||
List<Recipient> contacts = getContacts(query);
|
||||
List<Recipient> groups = getGroups(query);
|
||||
|
||||
callback.onComplete(new CameraContacts(recents, contacts, groups));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Recipient> getRecents(@NonNull String query) {
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Recipient> recipients = new ArrayList<>(RECENT_MAX);
|
||||
|
||||
try (ThreadDatabase.Reader threadReader = threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(RECENT_MAX))) {
|
||||
ThreadRecord threadRecord;
|
||||
while ((threadRecord = threadReader.getNext()) != null) {
|
||||
recipients.add(threadRecord.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Recipient> getContacts(@NonNull String query) {
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = contactsDatabase.queryTextSecureContacts(query)) {
|
||||
while (cursor.moveToNext()) {
|
||||
Address address = Address.fromExternal(context, cursor.getString(1));
|
||||
recipients.add(Recipient.from(context, address, false));
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Recipient> getGroups(@NonNull String query) {
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
try (GroupDatabase.Reader reader = groupDatabase.getGroupsFilteredByTitle(query)) {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
recipients.add(Recipient.from(context, Address.fromSerialized(groupRecord.getEncodedId()), false));
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onComplete(E result);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,6 @@ public interface CameraFragment {
|
|||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
||||
void onGalleryClicked();
|
||||
int getDisplayRotation();
|
||||
void onContinueClicked();
|
||||
void onCameraCountButtonClicked();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,7 +216,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
}
|
||||
|
||||
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
|
||||
|
||||
viewModel.onCameraControlsInitialized();
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import android.view.Menu;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
@ -29,20 +28,27 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
*/
|
||||
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
|
||||
|
||||
private static final String KEY_RECIPIENT_NAME = "recipient_name";
|
||||
private static final String KEY_TOOLBAR_TITLE = "toolbar_title";
|
||||
|
||||
private String recipientName;
|
||||
private String toolbarTitle;
|
||||
private MediaSendViewModel viewModel;
|
||||
private Controller controller;
|
||||
private GridLayoutManager layoutManager;
|
||||
|
||||
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Recipient recipient) {
|
||||
String name = Optional.fromNullable(recipient.getName())
|
||||
.or(Optional.fromNullable(recipient.getProfileName()))
|
||||
.or(recipient.toShortString());
|
||||
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Context context, @Nullable Recipient recipient) {
|
||||
String toolbarTitle;
|
||||
|
||||
if (recipient != null) {
|
||||
String name = Optional.fromNullable(recipient.getName())
|
||||
.or(Optional.fromNullable(recipient.getProfileName()))
|
||||
.or(recipient.toShortString());
|
||||
toolbarTitle = context.getString(R.string.MediaPickerActivity_send_to, name);
|
||||
} else {
|
||||
toolbarTitle = "";
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_RECIPIENT_NAME, name);
|
||||
args.putString(KEY_TOOLBAR_TITLE, toolbarTitle);
|
||||
|
||||
MediaPickerFolderFragment fragment = new MediaPickerFolderFragment();
|
||||
fragment.setArguments(args);
|
||||
|
@ -55,8 +61,8 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
toolbarTitle = getArguments().getString(KEY_TOOLBAR_TITLE);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -123,7 +129,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||
private void initToolbar(Toolbar toolbar) {
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(getString(R.string.MediaPickerActivity_send_to, recipientName));
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(toolbarTitle);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
}
|
||||
|
|
|
@ -35,26 +35,35 @@ import android.widget.Toast;
|
|||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.RevealState;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -67,6 +76,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -83,6 +93,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
MediaPickerItemFragment.Controller,
|
||||
ImageEditorFragment.Controller,
|
||||
CameraFragment.Controller,
|
||||
CameraContactSelectionFragment.Controller,
|
||||
ViewTreeObserver.OnGlobalLayoutListener,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
InputAwareLayout.OnKeyboardShownListener,
|
||||
|
@ -106,10 +117,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
private static final String TAG_ITEM_PICKER = "item_picker";
|
||||
private static final String TAG_SEND = "send";
|
||||
private static final String TAG_CAMERA = "camera";
|
||||
private static final String TAG_CONTACTS = "contacts";
|
||||
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
private @Nullable Recipient recipient;
|
||||
|
||||
private Recipient recipient;
|
||||
private TransportOption transport;
|
||||
private MediaSendViewModel viewModel;
|
||||
|
||||
|
@ -122,6 +133,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
private ViewGroup composeContainer;
|
||||
private ViewGroup countButton;
|
||||
private TextView countButtonText;
|
||||
private View continueButton;
|
||||
private ImageView revealButton;
|
||||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
|
@ -139,12 +151,20 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
*/
|
||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putExtra(KEY_ADDRESS, recipient.getAddress().serialize());
|
||||
intent.putExtra(KEY_ADDRESS, recipient.getAddress());
|
||||
intent.putExtra(KEY_TRANSPORT, transport);
|
||||
intent.putExtra(KEY_BODY, body);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildCameraFirstIntent(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putExtra(KEY_TRANSPORT, TransportOptions.getPushTransportOption(context));
|
||||
intent.putExtra(KEY_BODY, "");
|
||||
intent.putExtra(KEY_IS_CAMERA, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an intent to launch the media send flow starting with the picker.
|
||||
*/
|
||||
|
@ -169,11 +189,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.mediasend_activity);
|
||||
|
@ -192,6 +207,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
composeContainer = findViewById(R.id.mediasend_compose_container);
|
||||
countButton = findViewById(R.id.mediasend_count_button);
|
||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
||||
continueButton = findViewById(R.id.mediasend_continue_button);
|
||||
revealButton = findViewById(R.id.mediasend_reveal_toggle);
|
||||
captionText = findViewById(R.id.mediasend_caption);
|
||||
emojiToggle = findViewById(R.id.mediasend_emoji_toggle);
|
||||
|
@ -199,8 +215,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
mediaRail = findViewById(R.id.mediasend_media_rail);
|
||||
emojiDrawer = new Stub<>(findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
|
||||
Address address = getIntent().getParcelableExtra(KEY_ADDRESS);
|
||||
if (address != null) {
|
||||
recipient = Recipient.from(this, address, true);
|
||||
}
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||
|
||||
viewModel.setTransport(transport);
|
||||
|
@ -219,12 +239,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
} else if (!Util.isEmpty(media)) {
|
||||
viewModel.onSelectedMediaChanged(this, media);
|
||||
|
||||
Fragment fragment = MediaSendFragment.newInstance(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
Fragment fragment = MediaSendFragment.newInstance(Locale.getDefault());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.commit();
|
||||
} else {
|
||||
MediaPickerFolderFragment fragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerFolderFragment fragment = MediaPickerFolderFragment.newInstance(this, recipient);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
|
||||
.commit();
|
||||
|
@ -238,9 +258,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState());
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState(), processedMedia -> {
|
||||
setActivityResultAndFinish(processedMedia, composeText.getTextTrimmed(), transport);
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("No send fragment available!");
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -279,11 +301,13 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
sendButton.setTransport(transport);
|
||||
sendButton.disableTransport(transport.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
|
||||
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale()));
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(Locale.getDefault()));
|
||||
|
||||
composeText.append(viewModel.getBody());
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
if (recipient == null) {
|
||||
composeText.setHint(R.string.MediaSendActivity_message);
|
||||
} else if (recipient.isLocalNumber()) {
|
||||
composeText.setHint(getString(R.string.note_to_self), null);
|
||||
} else {
|
||||
String displayName = Optional.fromNullable(recipient.getName())
|
||||
|
@ -307,12 +331,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
initViewModel();
|
||||
|
||||
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicLanguage.onResume(this);
|
||||
continueButton.setOnClickListener(v -> navigateToContactSelect());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -345,7 +364,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
@Override
|
||||
public void onMediaSelected(@NonNull Media media) {
|
||||
viewModel.onSingleMediaSelected(this, media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -393,7 +412,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||
|
||||
viewModel.onImageCaptured(media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -403,13 +422,13 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onContinueClicked() {
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
public void onCameraCountButtonClicked() {
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGalleryClicked() {
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(this, recipient);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
|
@ -467,9 +486,22 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
navigateToCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraContactsSendClicked(@NonNull List<Recipient> recipients) {
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState(), processedMedia -> {
|
||||
sendMessages(recipients, processedMedia, composeText.getTextTrimmed(), transport);
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
}
|
||||
|
||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
||||
// TODO: Get actual folder title somehow
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(this, recipient);
|
||||
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
|
@ -483,35 +515,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.commit();
|
||||
}
|
||||
|
||||
public void onSendClicked(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
viewModel.onSendClicked();
|
||||
|
||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
||||
|
||||
if (mediaList.size() > 0) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
||||
intent.putExtra(EXTRA_MESSAGE, viewModel.getRevealDuration() == 0 ? message : "");
|
||||
intent.putExtra(EXTRA_TRANSPORT, transport);
|
||||
intent.putExtra(EXTRA_REVEAL_DURATION, viewModel.getRevealDuration());
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
|
||||
finish();
|
||||
|
||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
||||
}
|
||||
|
||||
public void onNoMediaAvailable() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void initViewModel() {
|
||||
viewModel.getHudState().observe(this, state -> {
|
||||
if (state == null) return;
|
||||
|
@ -535,13 +543,27 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
switch (state.getButtonState()) {
|
||||
case SEND:
|
||||
sendButtonContainer.setVisibility(View.VISIBLE);
|
||||
continueButton.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case COUNT:
|
||||
sendButtonContainer.setVisibility(View.GONE);
|
||||
continueButton.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.VISIBLE);
|
||||
countButtonText.setText(String.valueOf(state.getSelectionCount()));
|
||||
break;
|
||||
case CONTINUE:
|
||||
sendButtonContainer.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.GONE);
|
||||
continueButton.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!TextSecurePreferences.hasSeendCameraFirstTooltip(this)) {
|
||||
TooltipPopup.forTarget(continueButton)
|
||||
.setText(R.string.MediaSendActivity_select_recipients)
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
TextSecurePreferences.setHasSeenCameraFirstTooltip(this, true);
|
||||
}
|
||||
break;
|
||||
case GONE:
|
||||
sendButtonContainer.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.GONE);
|
||||
|
@ -624,8 +646,8 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
});
|
||||
}
|
||||
|
||||
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
|
||||
private void navigateToMediaSend(@NonNull Locale locale) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance(locale);
|
||||
String backstackTag = null;
|
||||
|
||||
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {
|
||||
|
@ -658,6 +680,27 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.execute();
|
||||
}
|
||||
|
||||
private void navigateToContactSelect() {
|
||||
if (hud.isInputOpen()) {
|
||||
hud.hideCurrentInput(composeText);
|
||||
}
|
||||
|
||||
Fragment contactFragment = CameraContactSelectionFragment.newInstance();
|
||||
Fragment editorFragment = getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
|
||||
if (editorFragment == null) {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.add(R.id.mediasend_fragment_container, contactFragment, TAG_CONTACTS)
|
||||
.hide(editorFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private Fragment getOrCreateCameraFragment() {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
||||
return fragment != null ? fragment : CameraFragment.newInstance();
|
||||
|
@ -675,7 +718,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
|
||||
charactersLeft.setText(String.format(Locale.getDefault(),
|
||||
"%d/%d (%d)",
|
||||
characterState.charactersRemaining,
|
||||
characterState.maxTotalMessageSize,
|
||||
|
@ -711,7 +754,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState) {
|
||||
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState, @NonNull OnProcessComplete callback) {
|
||||
Map<Media, EditorModel> modelsToRender = new HashMap<>();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
|
@ -784,7 +827,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
|
||||
@Override
|
||||
protected void onPostExecute(List<Media> media) {
|
||||
onSendClicked(media, composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
callback.onComplete(media);
|
||||
Util.cancelRunnableOnMain(progressTimer);
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
|
@ -798,6 +841,81 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
}
|
||||
|
||||
private void setActivityResultAndFinish(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
viewModel.onSendClicked();
|
||||
|
||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
||||
|
||||
if (mediaList.size() > 0) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
||||
intent.putExtra(EXTRA_MESSAGE, viewModel.getRevealDuration() == 0 ? message : "");
|
||||
intent.putExtra(EXTRA_TRANSPORT, transport);
|
||||
intent.putExtra(EXTRA_REVEAL_DURATION, viewModel.getRevealDuration());
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
|
||||
finish();
|
||||
|
||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
||||
}
|
||||
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull List<Media> media, @NonNull String body, @NonNull TransportOption transport) {
|
||||
SimpleTask.run(() -> {
|
||||
List<OutgoingSecureMediaMessage> messages = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
SlideDeck slideDeck = buildSlideDeck(media);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient,
|
||||
body,
|
||||
slideDeck.asAttachments(),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
recipient.getExpireMessages() * 1000,
|
||||
viewModel.getRevealDuration(),
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
messages.add(new OutgoingSecureMediaMessage(message));
|
||||
|
||||
// XXX We must do this to avoid sending out messages to the same recipient with the same
|
||||
// sentTimestamp. If we do this, they'll be considered dupes by the receiver.
|
||||
Util.sleep(5);
|
||||
}
|
||||
|
||||
MessageSender.sendMediaBroadcast(this, messages);
|
||||
return null;
|
||||
}, (nothing) -> {
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull SlideDeck buildSlideDeck(@NonNull List<Media> mediaList) {
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
for (Media mediaItem : mediaList) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
|
||||
} else {
|
||||
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
return slideDeck;
|
||||
}
|
||||
|
||||
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
|
||||
|
||||
int beforeLength;
|
||||
|
@ -838,4 +956,8 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
}
|
||||
|
||||
private interface OnProcessComplete {
|
||||
void onComplete(@NonNull List<Media> media);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.ControllableViewPager;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -29,8 +26,6 @@ public class MediaSendFragment extends Fragment {
|
|||
|
||||
private static final String TAG = MediaSendFragment.class.getSimpleName();
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
private static final String KEY_TRANSPORT = "transport";
|
||||
private static final String KEY_LOCALE = "locale";
|
||||
|
||||
private ViewGroup playbackControlsContainer;
|
||||
|
@ -40,10 +35,8 @@ public class MediaSendFragment extends Fragment {
|
|||
private MediaSendViewModel viewModel;
|
||||
|
||||
|
||||
public static MediaSendFragment newInstance(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
public static MediaSendFragment newInstance(@NonNull Locale locale) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_ADDRESS, recipient.getAddress());
|
||||
args.putParcelable(KEY_TRANSPORT, transport);
|
||||
args.putSerializable(KEY_LOCALE, locale);
|
||||
|
||||
MediaSendFragment fragment = new MediaSendFragment();
|
||||
|
@ -53,19 +46,17 @@ public class MediaSendFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return ThemeUtil.getThemedInflater(requireActivity(), inflater, R.style.TextSecure_DarkTheme)
|
||||
.inflate(R.layout.mediasend_fragment, container, false);
|
||||
return inflater.inflate(R.layout.mediasend_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initViewModel();
|
||||
fragmentPager = view.findViewById(R.id.mediasend_pager);
|
||||
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
|
||||
|
||||
|
@ -89,6 +80,9 @@ public class MediaSendFragment extends Fragment {
|
|||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden) {
|
||||
viewModel.onImageEditorStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
@ -17,7 +19,6 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.revealable.RevealableUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -58,7 +59,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
private int maxSelection;
|
||||
private Page page;
|
||||
private boolean isSms;
|
||||
private boolean isNoteToSelf;
|
||||
private Recipient recipient;
|
||||
private Optional<Media> lastCameraCapture;
|
||||
|
||||
private boolean hudVisible;
|
||||
|
@ -103,8 +104,8 @@ class MediaSendViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
void setRecipient(@NonNull Recipient recipient) {
|
||||
isNoteToSelf = recipient.isLocalNumber();
|
||||
void setRecipient(@Nullable Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
||||
|
@ -183,7 +184,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
hudVisible = true;
|
||||
composeVisible = revealState != RevealState.ENABLED;
|
||||
captionVisible = getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent());
|
||||
buttonState = ButtonState.SEND;
|
||||
buttonState = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
|
||||
|
||||
if (revealState == RevealState.GONE && revealSupported()) {
|
||||
revealState = TextSecurePreferences.isRevealableMessageEnabled(application) ? RevealState.ENABLED : RevealState.DISABLED;
|
||||
|
@ -244,6 +245,12 @@ class MediaSendViewModel extends ViewModel {
|
|||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onContactSelectStarted() {
|
||||
hudVisible = false;
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onRevealButtonToggled() {
|
||||
hudVisible = true;
|
||||
revealState = revealState == RevealState.ENABLED ? RevealState.DISABLED : RevealState.ENABLED;
|
||||
|
@ -266,7 +273,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
if (page != Page.EDITOR) return;
|
||||
|
||||
composeVisible = (revealState != RevealState.ENABLED);
|
||||
buttonState = ButtonState.SEND;
|
||||
buttonState = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
|
||||
|
||||
if (isSms) {
|
||||
railState = RailState.GONE;
|
||||
|
@ -289,7 +296,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
railState = RailState.GONE;
|
||||
composeVisible = (revealState == RevealState.GONE);
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.SEND;
|
||||
buttonState = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
|
||||
} else {
|
||||
if (isCaptionFocused) {
|
||||
railState = revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
|
||||
|
@ -300,7 +307,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
railState = revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
|
||||
composeVisible = (revealState != RevealState.ENABLED);
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.SEND;
|
||||
buttonState = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,11 +507,11 @@ class MediaSendViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
enum Page {
|
||||
CAMERA, ITEM_PICKER, FOLDER_PICKER, EDITOR, UNKNOWN
|
||||
CAMERA, ITEM_PICKER, FOLDER_PICKER, EDITOR, CONTACT_SELECT, UNKNOWN
|
||||
}
|
||||
|
||||
enum ButtonState {
|
||||
COUNT, SEND, GONE
|
||||
COUNT, SEND, CONTINUE, GONE
|
||||
}
|
||||
|
||||
enum RailState {
|
||||
|
|
|
@ -16,14 +16,20 @@
|
|||
*/
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
@ -43,6 +49,7 @@ import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
|||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
|
@ -52,6 +59,9 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
|||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessageSender {
|
||||
|
||||
|
@ -112,6 +122,100 @@ public class MessageSender {
|
|||
}
|
||||
}
|
||||
|
||||
public static void sendMediaBroadcast(@NonNull Context context, @NonNull List<OutgoingSecureMediaMessage> messages) {
|
||||
if (messages.isEmpty()) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - No messages!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidBroadcastList(messages)) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - Invalid message list!");
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
List<List<AttachmentId>> attachmentIds = new ArrayList<>(messages.get(0).getAttachments().size());
|
||||
List<Long> messageIds = new ArrayList<>(messages.size());
|
||||
|
||||
for (int i = 0; i < messages.get(0).getAttachments().size(); i++) {
|
||||
attachmentIds.add(new ArrayList<>(messages.size()));
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
mmsDatabase.beginTransaction();
|
||||
|
||||
for (OutgoingSecureMediaMessage message : messages) {
|
||||
long allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
long messageId = mmsDatabase.insertMessageOutbox(message, allocatedThreadId, false, null);
|
||||
List<DatabaseAttachment> attachments = attachmentDatabase.getAttachmentsForMessage(messageId);
|
||||
|
||||
if (attachments.size() != attachmentIds.size()) {
|
||||
Log.w(TAG, "Got back an attachment list that was a different size than expected. Expected: " + attachmentIds.size() + " Actual: "+ attachments.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < attachments.size(); i++) {
|
||||
attachmentIds.get(i).add(attachments.get(i).getAttachmentId());
|
||||
}
|
||||
|
||||
messageIds.add(messageId);
|
||||
}
|
||||
|
||||
mmsDatabase.setTransactionSuccessful();
|
||||
} finally {
|
||||
mmsDatabase.endTransaction();
|
||||
}
|
||||
|
||||
List<AttachmentUploadJob> uploadJobs = new ArrayList<>(attachmentIds.size());
|
||||
List<AttachmentCopyJob> copyJobs = new ArrayList<>(attachmentIds.size());
|
||||
List<Job> messageJobs = new ArrayList<>(attachmentIds.get(0).size());
|
||||
|
||||
for (List<AttachmentId> idList : attachmentIds) {
|
||||
uploadJobs.add(new AttachmentUploadJob(idList.get(0)));
|
||||
|
||||
if (idList.size() > 1) {
|
||||
AttachmentId sourceId = idList.get(0);
|
||||
List<AttachmentId> destinationIds = idList.subList(1, idList.size());
|
||||
|
||||
copyJobs.add(new AttachmentCopyJob(sourceId, destinationIds));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < messageIds.size(); i++) {
|
||||
long messageId = messageIds.get(i);
|
||||
OutgoingSecureMediaMessage message = messages.get(i);
|
||||
Recipient recipient = message.getRecipient();
|
||||
|
||||
if (isLocalSelfSend(context, recipient, false)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (isGroupPushSend(recipient)) {
|
||||
messageJobs.add(new PushGroupSendJob(messageId, recipient.getAddress(), null));
|
||||
} else {
|
||||
messageJobs.add(new PushMediaSendJob(messageId, recipient.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "sendMediaBroadcast() - Uploading %d attachment(s), copying %d of them, then sending %d messages.",
|
||||
uploadJobs.size(),
|
||||
copyJobs.size(),
|
||||
messageJobs.size()));
|
||||
|
||||
JobManager.Chain chain = ApplicationContext.getInstance(context).getJobManager().startChain(uploadJobs);
|
||||
|
||||
if (copyJobs.size() > 0) {
|
||||
chain = chain.then(copyJobs);
|
||||
}
|
||||
|
||||
chain = chain.then(messageJobs);
|
||||
chain.enqueue();
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - Failed to send messages!", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resendGroupMessage(Context context, MessageRecord messageRecord, Address filterAddress) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterAddress);
|
||||
|
@ -292,4 +396,20 @@ public class MessageSender {
|
|||
Log.w("Failed to update self-sent message.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidBroadcastList(@NonNull List<OutgoingSecureMediaMessage> messages) {
|
||||
if (messages.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int attachmentSize = messages.get(0).getAttachments().size();
|
||||
|
||||
for (OutgoingSecureMediaMessage message : messages) {
|
||||
if (message.getAttachments().size() != attachmentSize) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.stickers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
@ -11,7 +10,6 @@ import android.view.ViewGroup;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
@ -75,8 +73,8 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
}
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(@NonNull StickerSection section, @NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
section.bindViewHolder(viewHolder, position, glideRequests, eventListener);
|
||||
public void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull StickerSection section, int localPosition) {
|
||||
section.bindViewHolder(viewHolder, localPosition, glideRequests, eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -166,12 +164,10 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
}
|
||||
|
||||
void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
int globalPosition,
|
||||
int localPosition,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener)
|
||||
{
|
||||
int localPosition = getLocalPosition(globalPosition);
|
||||
|
||||
if (localPosition == 0) {
|
||||
((HeaderViewHolder) viewHolder).bind(titleResId);
|
||||
} else if (records.isEmpty()) {
|
||||
|
|
|
@ -185,6 +185,8 @@ public class TextSecurePreferences {
|
|||
|
||||
private static final String REVEALABLE_MESSAGE_DEFAULT = "pref_revealable_message_default";
|
||||
|
||||
private static final String SEEN_CAMERA_FIRST_TOOLTIP = "pref_seen_camera_first_tooltip";
|
||||
|
||||
public static boolean isScreenLockEnabled(@NonNull Context context) {
|
||||
return getBooleanPreference(context, SCREEN_LOCK, false);
|
||||
}
|
||||
|
@ -1108,6 +1110,14 @@ public class TextSecurePreferences {
|
|||
return getBooleanPreference(context, REVEALABLE_MESSAGE_DEFAULT, false);
|
||||
}
|
||||
|
||||
public static void setHasSeenCameraFirstTooltip(Context context, boolean value) {
|
||||
setBooleanPreference(context, SEEN_CAMERA_FIRST_TOOLTIP, value);
|
||||
}
|
||||
|
||||
public static boolean hasSeendCameraFirstTooltip(Context context) {
|
||||
return getBooleanPreference(context, SEEN_CAMERA_FIRST_TOOLTIP, false);
|
||||
}
|
||||
|
||||
public static void setBooleanPreference(Context context, String key, boolean value) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
|
|
@ -542,6 +542,14 @@ public class Util {
|
|||
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1024, digitGroups)) + " " + units[digitGroups];
|
||||
}
|
||||
|
||||
public static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Handler getHandler() {
|
||||
if (handler == null) {
|
||||
synchronized (Util.class) {
|
||||
|
|
|
@ -34,7 +34,7 @@ public abstract class SectionedRecyclerViewAdapter<IdType, SectionImpl extends S
|
|||
protected @NonNull abstract RecyclerView.ViewHolder createHeaderViewHolder(@NonNull ViewGroup parent);
|
||||
protected @NonNull abstract RecyclerView.ViewHolder createContentViewHolder(@NonNull ViewGroup parent);
|
||||
protected @Nullable abstract RecyclerView.ViewHolder createEmptyViewHolder(@NonNull ViewGroup viewGroup);
|
||||
protected abstract void bindViewHolder(@NonNull SectionImpl section, @NonNull RecyclerView.ViewHolder holder, int position);
|
||||
protected abstract void bindViewHolder(@NonNull RecyclerView.ViewHolder holder, @NonNull SectionImpl section, int localPosition);
|
||||
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||
|
@ -55,30 +55,30 @@ public abstract class SectionedRecyclerViewAdapter<IdType, SectionImpl extends S
|
|||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
public long getItemId(int globalPosition) {
|
||||
for (SectionImpl section: getSections()) {
|
||||
if (section.handles(position)) {
|
||||
return section.getItemId(stableIdGenerator, position);
|
||||
if (section.handles(globalPosition)) {
|
||||
return section.getItemId(stableIdGenerator, globalPosition);
|
||||
}
|
||||
}
|
||||
throw new NoSectionException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
public int getItemViewType(int globalPosition) {
|
||||
for (SectionImpl section : getSections()) {
|
||||
if (section.handles(position)) {
|
||||
return section.getViewType(position);
|
||||
if (section.handles(globalPosition)) {
|
||||
return section.getViewType(globalPosition);
|
||||
}
|
||||
}
|
||||
throw new NoSectionException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int globalPosition) {
|
||||
for (SectionImpl section : getSections()) {
|
||||
if (section.handles(position)) {
|
||||
bindViewHolder(section, holder, position);
|
||||
if (section.handles(globalPosition)) {
|
||||
bindViewHolder(holder, section, section.getLocalPosition(globalPosition));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -106,12 +106,12 @@ public abstract class SectionedRecyclerViewAdapter<IdType, SectionImpl extends S
|
|||
public abstract int getContentSize();
|
||||
public abstract long getItemId(@NonNull StableIdGenerator<E> idGenerator, int globalPosition);
|
||||
|
||||
protected int getLocalPosition(int globalPosition) {
|
||||
protected final int getLocalPosition(int globalPosition) {
|
||||
return globalPosition - offset;
|
||||
}
|
||||
|
||||
public int getViewType(int globalPosition) {
|
||||
int localPosition = globalPosition - offset;
|
||||
final int getViewType(int globalPosition) {
|
||||
int localPosition = getLocalPosition(globalPosition);
|
||||
|
||||
if (localPosition == 0) {
|
||||
return TYPE_HEADER;
|
||||
|
@ -122,12 +122,12 @@ public abstract class SectionedRecyclerViewAdapter<IdType, SectionImpl extends S
|
|||
}
|
||||
}
|
||||
|
||||
public boolean handles(int globalPosition) {
|
||||
int localPosition = globalPosition - offset;
|
||||
final boolean handles(int globalPosition) {
|
||||
int localPosition = getLocalPosition(globalPosition);
|
||||
return localPosition >= 0 && localPosition < size();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
public final int size() {
|
||||
if (getContentSize() == 0 && hasEmptyState()) {
|
||||
return 2;
|
||||
} else if (getContentSize() == 0) {
|
||||
|
|
Loading…
Add table
Reference in a new issue