Compare commits

..

78 Commits

Author SHA1 Message Date
Alan Evans
881a1edccb Bump version to 4.64.5 2020-06-22 10:53:52 -03:00
Alan Evans
1b7b574289 Updated language translations. 2020-06-22 10:50:27 -03:00
Alan Evans
d1d7498447 Fix text colors when system theme doesn't match. 2020-06-22 10:02:18 -03:00
Greyson Parrelli
50c18727e7 Bump version to 4.64.4 2020-06-21 12:23:31 -04:00
Greyson Parrelli
e9bfde470a Updated language translations. 2020-06-21 12:23:10 -04:00
Greyson Parrelli
68f718a210 Fix issue with conversation list times not updating.
Just started calling notifyDataSetChanged() in onResume() to provide
some sort of time update regularity.
2020-06-21 12:20:18 -04:00
Greyson Parrelli
c3e528ad4b Bump version to 4.64.3 2020-06-19 19:17:16 -04:00
Greyson Parrelli
28af97c400 Updated language translations. 2020-06-19 19:17:16 -04:00
Jim Gustafson
c2e4c343ab Update to ringrtc v2.1.1 2020-06-19 19:12:59 -04:00
Cody Henthorne
8a78589c2f Fix light navigation buttons in conversation settings screens. 2020-06-19 16:53:38 -04:00
Alan Evans
841ee18435 Add default option to message vibrate for pre API26. 2020-06-19 13:08:54 -03:00
Greyson Parrelli
71f54701d2 Add additional safeguards around disappearing messages. 2020-06-19 10:17:23 -04:00
Alan Evans
1c99939dfa Bump version to 4.64.2 2020-06-18 17:30:38 -03:00
Alan Evans
50462cecd0 Updated language translations. 2020-06-18 17:29:20 -03:00
Cody Henthorne
aa6a32f023 Make conversation footer always show. 2020-06-18 16:14:38 -04:00
Alan Evans
c4dc9064e3 Handle Attachment Keyboard selection of a too large item. 2020-06-18 15:55:26 -03:00
Alan Evans
bc5be10a0e Respect emoji config on conversation banner title. 2020-06-18 15:39:02 -03:00
Alan Evans
98d9b57379 Add copy to bottom sheet for Note to Self. 2020-06-18 14:34:30 -03:00
Cody Henthorne
021a16050a Stop back transition jank from avatar viewer to settings. 2020-06-18 13:16:08 -04:00
Alan Evans
555104aff0 Make message button navigate back if launched from the conversation. 2020-06-18 14:00:06 -03:00
Alan Evans
95d63b78f4 Add call and message buttons to recipient bottom sheet.
And insecure call button for non-registered contacts.
2020-06-18 13:23:46 -03:00
Alan Evans
80f9e1f4f1 Fix not able to get to archived conversations when all archived. 2020-06-18 12:23:20 -03:00
Alan Evans
a77997a4de Fix margins for "No groups in common" & unregistered case. 2020-06-18 09:49:22 -03:00
Alan Evans
ec4eb8e2a9 Bump version to 4.64.1 2020-06-17 17:54:58 -03:00
Alan Evans
1bdeade71e Updated language translations. 2020-06-17 17:53:19 -03:00
Greyson Parrelli
629ba105cb Detect real age of call request by using server timestamps. 2020-06-17 17:53:18 -03:00
Alan Evans
891a1af995 Show Note to Self for local number recipient preferences. 2020-06-17 17:49:44 -03:00
Cody Henthorne
0fbc6ac151 Revert improperly removed code for Message Request footer. 2020-06-17 17:49:43 -03:00
Alan Evans
a6384d1b73 Add insecure call ability to recipient settings. 2020-06-17 17:49:43 -03:00
Alan Evans
2fb9514890 Respect emoji setting in profile/group name editing. 2020-06-17 17:49:43 -03:00
Alan Evans
fe89794505 Hide recipient subtitle if no name/username set. 2020-06-17 17:49:43 -03:00
Cody Henthorne
08800c9faf Make Message Details update views in more situations. 2020-06-17 17:49:43 -03:00
Cody Henthorne
469a4700d2 Fix improper tinting on screens when using FallbackPhoto. 2020-06-17 17:49:43 -03:00
Alan Evans
6707f974a5 Remove NewGroupUI FeatureFlag. 2020-06-17 17:49:43 -03:00
Alan Evans
c122cada2b Change call button shade. 2020-06-17 17:49:43 -03:00
Alan Evans
96f02d8c95 Hide some views for Note to Self conversation. 2020-06-17 17:49:43 -03:00
Greyson Parrelli
dd717b60b8 Bump version to 4.64.0 2020-06-16 23:47:15 -04:00
Greyson Parrelli
3c20c7f4b4 Updated language translations. 2020-06-16 23:46:41 -04:00
Cody Henthorne
1a09e70a04 Remove old Message Details. 2020-06-16 19:30:35 -04:00
Alan Evans
027453bbd2 Prevent IllegalStateException on recipient bottom sheet. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
b621efa4a5 Don't prefetch views for the conversation list. 2020-06-16 19:30:35 -04:00
Cody Henthorne
2915e4698c Show registration rate limit error messaging. 2020-06-16 19:30:35 -04:00
Cody Henthorne
b687b1a4c5 Fix repeat alerts by using explicit reminder intent. 2020-06-16 19:30:35 -04:00
Alan Evans
b53827f32b Manage recipient activity. 2020-06-16 19:30:35 -04:00
Cody Henthorne
d9641128a8 Refresh Message Details screen. 2020-06-16 19:30:35 -04:00
Alan Evans
dfb5562142 Use group manager for MMS groups. 2020-06-16 19:30:35 -04:00
Jim Gustafson
d467c04749 Ensure speaker off at start of any call 2020-06-16 19:30:35 -04:00
Greyson Parrelli
3d7cffef2b Remove Message Requests feature flag. 2020-06-16 19:30:35 -04:00
Alex Hart
f2fe81d9b5 Fix conversation jumping when loading at last scroll position. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
cf98a22269 Add placeholder support for ConversationListAdapter. 2020-06-16 19:30:35 -04:00
Alex Hart
49f75d7036 Migrate ConversationList to paging library and apply abstractions to conversation. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
ce940235b0 Optimistically fetch profiles. 2020-06-16 19:30:35 -04:00
Alan Evans
f5626f678d Make CustomNotificationsDialogFragment work with recipients. 2020-06-16 19:30:35 -04:00
Alan Evans
b3a59c3946 Use recipient display name in recipient bottom sheet. 2020-06-16 19:30:35 -04:00
Fumiaki Yoshimatsu
93c390c4fc Don't send a read receipt when the recipient is blocked.
Fixes #9610
2020-06-16 19:30:35 -04:00
Cody Henthorne
941ab5a98f Prevent avatar from showing a start of outgoing video call. 2020-06-16 19:30:35 -04:00
Jim Gustafson
2ecdf803c0 Update to ringrtc v2.1.0 2020-06-16 19:30:35 -04:00
Cody Henthorne
5b2a399392 Return to previous scroll position when returning to a conversation. 2020-06-16 19:30:35 -04:00
Alex Hart
a9ea1d7606 Utilize DayNight theme when launching the app. 2020-06-12 11:36:15 -03:00
Greyson Parrelli
1ce8ac2de6 Light refactor of SignalStore. 2020-06-12 11:36:15 -03:00
Greyson Parrelli
e2019579fb Bump version to 4.63.3 2020-06-12 10:09:20 -04:00
Greyson Parrelli
fb3c6e56ee Updated language translations. 2020-06-12 10:08:51 -04:00
Greyson Parrelli
3fad007ae0 Cancel typing jobs when you send a group message. 2020-06-12 10:06:20 -04:00
Greyson Parrelli
8891b6c930 Properly throw UnregisteredUserException in SignalServicePipe. 2020-06-11 12:08:40 -04:00
Alan Evans
400c592acf Display 'Unknown group' for groups with no name. 2020-06-10 17:17:47 -03:00
Alex Hart
e13f3254ad Fix message jump-to-position. 2020-06-10 17:06:40 -03:00
Greyson Parrelli
bf40a07bb9 Bump version to 4.63.2 2020-06-10 14:43:24 -04:00
Greyson Parrelli
8f3a6b8479 Update unblock string. 2020-06-10 14:37:03 -04:00
Greyson Parrelli
7642b7cc72 Fix issue with typing indicators in blocked groups. 2020-06-10 14:28:12 -04:00
Greyson Parrelli
e12ea60d85 Bump version to 4.63.1 2020-06-10 12:48:15 -04:00
Greyson Parrelli
0b13c4aed6 Updated language translations. 2020-06-10 12:48:15 -04:00
Alan Evans
47919382e9 Show 'Add to another group' when launched from a group context. 2020-06-10 12:59:57 -03:00
Greyson Parrelli
d60d67ee7e Set contact colors more aggressively. 2020-06-10 10:49:22 -04:00
Alan Evans
559aa687a5 Show group participants menu item on a MMS group. 2020-06-10 11:32:50 -03:00
Cody Henthorne
bc0761f002 Fix navigate up behavior for Conversations. 2020-06-10 10:28:34 -04:00
Alan Evans
c0c2fc0eba When there are no recipients left on group create screen toast and return to list. 2020-06-10 09:07:12 -03:00
Alan Evans
44fe43c74c Hide 'Add to a group' for non-registered users. 2020-06-10 08:54:57 -03:00
Alan Evans
53a2a5d693 Prevent highlighter opacity affecting blur tool. 2020-06-09 23:56:03 -03:00
265 changed files with 10113 additions and 7973 deletions

View File

@@ -80,8 +80,8 @@ protobuf {
}
}
def canonicalVersionCode = 653
def canonicalVersionName = "4.63.0"
def canonicalVersionCode = 662
def canonicalVersionName = "4.64.5"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -304,7 +304,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.0.3'
implementation 'org.signal:ringrtc-android:2.1.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -151,7 +151,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.ShareActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
@@ -184,7 +184,7 @@
</activity>
<activity android:name=".stickers.StickerPackPreviewActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
@@ -243,24 +243,24 @@
android:theme="@style/TextSecure.LightTheme.Popup"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".MessageDetailsActivity"
<activity android:name=".messagedetails.MessageDetailsActivity"
android:label="@string/AndroidManifest__message_details"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
@@ -274,7 +274,7 @@
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -284,7 +284,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".NewConversationActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -294,7 +294,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".giph.ui.GiphyActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -359,7 +359,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -410,10 +410,6 @@
</activity>
<activity android:name=".RecipientPreferenceActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -462,44 +458,44 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactNameEditActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.SharedContactDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShortcutLauncherActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".maps.PlacePickerActivity"
android:label="@string/PlacePickerActivity_title"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MainActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.addtogroup.AddToGroupsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
@@ -700,11 +696,7 @@
</intent-filter>
</receiver>
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
<receiver android:name=".notifications.DeleteNotificationReceiver">
<intent-filter>

View File

@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
@@ -133,6 +134,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this);
RegistrationUtil.markRegistrationPossiblyComplete();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);

View File

@@ -272,12 +272,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
requireActivity().startActivity(intent);
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
return true;
}
}

View File

@@ -98,7 +98,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
})
.into(avatar);
toolbar.setTitle(recipient.toShortString(context));
toolbar.setTitle(recipient.getDisplayName(context));
});
avatar.setOnClickListener(v -> toggleUiVisibility());

View File

@@ -14,4 +14,7 @@ public interface BindableConversationListItem extends Unbindable {
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull Set<Long> selectedThreads, boolean batchMode);
void setBatchMode(boolean batchMode);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
}

View File

@@ -51,7 +51,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
super(context);
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
String name = recipient.toShortString(context);
String name = recipient.getDisplayName(context);
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
SpannableString spannableString = new SpannableString(introduction + " " +
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
@@ -175,7 +175,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
messageRecord.getDateSent(),
legacy ? Base64.decode(messageRecord.getBody()) : null,
!legacy ? Base64.decode(messageRecord.getBody()) : null,
0, null);
0,
0,
null);
long pushId = pushDatabase.insert(envelope);

View File

@@ -271,7 +271,7 @@ public final class ContactSelectionListFragment extends Fragment
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
if (listCallback != null && FeatureFlags.newGroupUI()) {
if (listCallback != null) {
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
} else {
@@ -518,7 +518,7 @@ public final class ContactSelectionListFragment extends Fragment
private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
if (isMulti() && FeatureFlags.newGroupUI()) {
if (isMulti()) {
addChipForSelectedContact(selectedContact);
}
}

View File

@@ -1,629 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.components.PushRecipientsPanel;
import org.thoughtcrime.securesms.components.PushRecipientsPanel.RecipientsPanelChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Activity to create and update {@link GroupId.V1} groups
*
* @author Jake McGinty
*/
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements OnRecipientDeletedListener,
RecipientsPanelChangedListener
{
private final static String TAG = GroupCreateActivity.class.getSimpleName();
private static final String GROUP_ID_EXTRA = "group_id";
private static final String GROUP_THREAD_EXTRA = "group_thread";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
private static final int PICK_CONTACT = 1;
private EditText groupName;
private ListView listView;
private ImageView avatar;
private TextView creatingText;
private Bitmap avatarBmp;
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
Intent intent = new Intent(context, GroupCreateActivity.class);
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle state, boolean ready) {
setContentView(R.layout.group_create_activity);
//noinspection ConstantConditions
initializeAppBar();
initializeResources();
initializeExistingGroup();
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
updateViewState();
}
private boolean isSignalGroup() {
return TextSecurePreferences.isPushRegistered(this) && !getAdapter().hasNonPushMembers();
}
private void disableSignalGroupViews(int reasonResId) {
View pushDisabled = findViewById(R.id.push_disabled);
pushDisabled.setVisibility(View.VISIBLE);
((TextView) findViewById(R.id.push_disabled_reason)).setText(reasonResId);
avatar.setEnabled(false);
groupName.setEnabled(false);
}
private void enableSignalGroupViews() {
findViewById(R.id.push_disabled).setVisibility(View.GONE);
avatar.setEnabled(true);
groupName.setEnabled(true);
}
@SuppressWarnings("ConstantConditions")
private void updateViewState() {
if (!TextSecurePreferences.isPushRegistered(this)) {
disableSignalGroupViews(R.string.GroupCreateActivity_youre_not_registered_for_signal);
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
} else if (getAdapter().hasNonPushMembers()) {
disableSignalGroupViews(R.string.GroupCreateActivity_contacts_dont_support_push);
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
} else {
enableSignalGroupViews();
getSupportActionBar().setTitle(groupToUpdate.isPresent()
? R.string.GroupCreateActivity_actionbar_edit_title
: R.string.GroupCreateActivity_actionbar_title);
}
}
private static boolean isActiveInDirectory(Recipient recipient) {
return recipient.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED;
}
private void addSelectedContacts(@NonNull Recipient... recipients) {
new AddMembersTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipients);
}
private void addSelectedContacts(@NonNull Collection<Recipient> recipients) {
addSelectedContacts(recipients.toArray(new Recipient[recipients.size()]));
}
private void initializeAppBar() {
Drawable upIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24);
getSupportActionBar().setHomeAsUpIndicator(upIcon);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void initializeResources() {
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
listView = findViewById(R.id.selected_contacts_list);
avatar = findViewById(R.id.avatar);
groupName = findViewById(R.id.group_name);
creatingText = findViewById(R.id.creating_group_text);
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
adapter.setOnRecipientDeletedListener(this);
listView.setAdapter(adapter);
recipientsEditor.setHint(R.string.recipients_panel__add_members);
recipientsPanel.setPanelChangeListener(this);
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
avatar.setImageDrawable(getDefaultGroupAvatar());
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
}
private Drawable getDefaultGroupAvatar() {
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
}
private void initializeExistingGroup() {
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
if (groupId != null) {
GroupId.V1 groupIdV1 = groupId.requireV1();
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
if (FeatureFlags.newGroupUI()) {
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
}
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.group_create, menu);
super.onPrepareOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_create_group:
if (groupToUpdate.isPresent()) handleGroupUpdate();
else handleGroupCreate();
return true;
}
return false;
}
@Override
public void onRecipientDeleted(Recipient recipient) {
getAdapter().remove(recipient);
updateViewState();
}
@Override
public void onRecipientsPanelUpdate(List<Recipient> recipients) {
if (recipients != null && !recipients.isEmpty()) addSelectedContacts(recipients);
}
private void handleGroupCreate() {
if (getAdapter().getCount() < 1) {
Log.i(TAG, getString(R.string.GroupCreateActivity_contacts_no_members));
Toast.makeText(getApplicationContext(), R.string.GroupCreateActivity_contacts_no_members, Toast.LENGTH_SHORT).show();
return;
}
if (isSignalGroup()) {
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private void handleGroupUpdate() {
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void handleOpenConversation(long threadId, Recipient recipient) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
startActivity(intent);
finish();
}
private SelectedRecipientsAdapter getAdapter() {
return (SelectedRecipientsAdapter) listView.getAdapter();
}
private @Nullable String getGroupName() {
return groupName.getText() != null ? groupName.getText().toString() : null;
}
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
if (data == null || resultCode != Activity.RESULT_OK)
return;
switch (reqCode) {
case PICK_CONTACT:
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
for (RecipientId contact : selected) {
Recipient recipient = Recipient.resolved(contact);
addSelectedContacts(recipient);
}
break;
case REQUEST_CODE_SELECT_AVATAR:
if (data.getBooleanExtra("delete", false)) {
avatarBmp = null;
avatar.setImageDrawable(getDefaultGroupAvatar());
return;
}
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
GlideApp.with(this)
.asBitmap()
.load(decryptableUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerCrop()
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
setAvatar(decryptableUri, resource);
}
});
}
}
private class AddRecipientButtonListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class);
if (groupToUpdate.isPresent()) {
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH);
} else {
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH | DisplayMode.FLAG_SMS);
}
startActivityForResult(intent, PICK_CONTACT);
}
}
private static class CreateMmsGroupTask extends AsyncTask<Void,Void,GroupActionResult> {
private final GroupCreateActivity activity;
private final Set<Recipient> members;
public CreateMmsGroupTask(GroupCreateActivity activity, Set<Recipient> members) {
this.activity = activity;
this.members = members;
}
@Override
protected GroupActionResult doInBackground(Void... avoid) {
List<RecipientId> memberAddresses = new LinkedList<>();
for (Recipient recipient : members) {
memberAddresses.add(recipient.getId());
}
memberAddresses.add(Recipient.self().getId());
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
return new GroupActionResult(groupRecipient, threadId);
}
@Override
protected void onPostExecute(GroupActionResult result) {
activity.handleOpenConversation(result.getThreadId(), result.getGroupRecipient());
}
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
}
private abstract static class SignalGroupTask extends AsyncTask<Void,Void,Optional<GroupActionResult>> {
protected GroupCreateActivity activity;
protected Bitmap avatar;
protected Set<Recipient> members;
protected String name;
public SignalGroupTask(GroupCreateActivity activity,
Bitmap avatar,
String name,
Set<Recipient> members)
{
this.activity = activity;
this.avatar = avatar;
this.name = name;
this.members = members;
}
@Override
protected void onPreExecute() {
activity.findViewById(R.id.group_details_layout).setVisibility(View.GONE);
activity.findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE);
activity.findViewById(R.id.menu_create_group).setVisibility(View.GONE);
final int titleResId = activity.groupToUpdate.isPresent()
? R.string.GroupCreateActivity_updating_group
: R.string.GroupCreateActivity_creating_group;
activity.creatingText.setText(activity.getString(titleResId, activity.getGroupName()));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> groupActionResultOptional) {
if (activity.isFinishing()) return;
activity.findViewById(R.id.group_details_layout).setVisibility(View.VISIBLE);
activity.findViewById(R.id.creating_group_layout).setVisibility(View.GONE);
activity.findViewById(R.id.menu_create_group).setVisibility(View.VISIBLE);
}
}
private static class CreateSignalGroupTask extends SignalGroupTask {
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) {
super(activity, avatar, name, members);
}
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> result) {
if (result.isPresent() && result.get().getThreadId() > -1) {
if (!activity.isFinishing()) {
activity.handleOpenConversation(result.get().getThreadId(), result.get().getGroupRecipient());
}
} else {
super.onPostExecute(result);
Toast.makeText(activity.getApplicationContext(),
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
}
}
}
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
private final GroupId.V1 groupId;
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
Bitmap avatar, String name, Set<Recipient> members)
{
super(activity, avatar, name, members);
this.groupId = groupId;
}
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> result) {
if (result.isPresent() && result.get().getThreadId() > -1) {
if (!activity.isFinishing()) {
Intent intent = activity.getIntent();
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
activity.setResult(RESULT_OK, intent);
activity.finish();
}
} else {
super.onPostExecute(result);
Toast.makeText(activity.getApplicationContext(),
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
}
}
}
private static class AddMembersTask extends AsyncTask<Recipient,Void,List<AddMembersTask.Result>> {
static class Result {
Optional<Recipient> recipient;
boolean isPush;
String reason;
public Result(@Nullable Recipient recipient, boolean isPush, @Nullable String reason) {
this.recipient = Optional.fromNullable(recipient);
this.isPush = isPush;
this.reason = reason;
}
}
private GroupCreateActivity activity;
private boolean failIfNotPush;
public AddMembersTask(@NonNull GroupCreateActivity activity) {
this.activity = activity;
this.failIfNotPush = activity.groupToUpdate.isPresent();
}
@Override
protected List<Result> doInBackground(Recipient... recipients) {
final List<Result> results = new LinkedList<>();
for (Recipient recipient : recipients) {
boolean isPush = isActiveInDirectory(recipient);
if (failIfNotPush && !isPush) {
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
recipient.toShortString(activity))));
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getE164().or(""))) {
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_youre_already_in_the_group)));
} else {
results.add(new Result(recipient, isPush, null));
}
}
return results;
}
@Override
protected void onPostExecute(List<Result> results) {
if (activity.isFinishing()) return;
for (Result result : results) {
if (result.recipient.isPresent()) {
activity.getAdapter().add(result.recipient.get(), result.isPush);
} else {
Toast.makeText(activity, result.reason, Toast.LENGTH_SHORT).show();
}
}
activity.updateViewState();
}
}
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
private GroupCreateActivity activity;
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
super(activity,
R.string.GroupCreateActivity_loading_group_details,
R.string.please_wait);
this.activity = activity;
}
@Override
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
existingContacts.addAll(recipients);
if (group.isPresent()) {
Bitmap avatar = null;
try {
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar.");
}
return Optional.of(new GroupData(groupIds[0],
existingContacts,
avatar,
BitmapUtil.toByteArray(avatar),
group.get().getTitle()));
} else {
return Optional.absent();
}
}
@Override
protected void onPostExecute(Optional<GroupData> group) {
super.onPostExecute(group);
if (group.isPresent() && !activity.isFinishing()) {
activity.groupToUpdate = group;
activity.groupName.setText(group.get().name);
if (group.get().avatarBmp != null) {
activity.setAvatar(group.get().avatarBytes, group.get().avatarBmp);
}
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
adapter.setOnRecipientDeletedListener(activity);
activity.listView.setAdapter(adapter);
activity.updateViewState();
}
}
}
private <T> void setAvatar(T model, Bitmap bitmap) {
avatarBmp = bitmap;
GlideApp.with(this)
.load(model)
.circleCrop()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(avatar);
}
private static class GroupData {
GroupId.V1 id;
Set<Recipient> recipients;
Bitmap avatarBmp;
byte[] avatarBytes;
String name;
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
this.id = id;
this.recipients = recipients;
this.avatarBmp = avatarBmp;
this.avatarBytes = avatarBytes;
this.name = name;
}
}
}

View File

@@ -181,7 +181,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
String from;
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
else if (mediaItem.recipient != null) from = mediaItem.recipient.toShortString(this);
else if (mediaItem.recipient != null) from = mediaItem.recipient.getDisplayName(this);
else from = "";
if (showThread) {
@@ -193,7 +193,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
if (threadRecipient.isLocalNumber()) {
from = getString(R.string.note_to_self);
} else {
to = threadRecipient.toShortString(this);
to = threadRecipient.getDisplayName(this);
}
} else {
to = getString(R.string.MediaPreviewActivity_you);

View File

@@ -1,451 +0,0 @@
/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.lang.ref.WeakReference;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
/**
* @author Jake McGinty
*/
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
public static final String MESSAGE_ID_EXTRA = "message_id";
public static final String THREAD_ID_EXTRA = "thread_id";
public static final String IS_PUSH_GROUP_EXTRA = "is_push_group";
public static final String TYPE_EXTRA = "type";
public static final String RECIPIENT_EXTRA = "recipient_id";
private GlideRequests glideRequests;
private long threadId;
private boolean isPushGroup;
private ConversationItem conversationItem;
private ViewGroup itemParent;
private View metadataContainer;
private View expiresContainer;
private TextView errorText;
private View resendButton;
private TextView sentDate;
private TextView receivedDate;
private TextView expiresInText;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
private ListView recipientsList;
private LayoutInflater inflater;
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean running;
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.message_details_activity);
running = true;
initializeResources();
initializeActionBar();
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
assert getSupportActionBar() != null;
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
}
@Override
protected void onPause() {
super.onPause();
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
}
@Override
protected void onDestroy() {
super.onDestroy();
running = false;
}
private void initializeActionBar() {
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
recipient.observe(this, r -> setActionBarColor(r.getColor()));
setActionBarColor(recipient.get().getColor());
}
private void setActionBarColor(MaterialColor color) {
assert getSupportActionBar() != null;
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color.toStatusBarColor(this));
}
}
private void initializeResources() {
inflater = LayoutInflater.from(this);
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
glideRequests = GlideApp.with(this);
itemParent = header.findViewById(R.id.item_container);
recipientsList = findViewById(R.id.recipients_list);
metadataContainer = header.findViewById(R.id.metadata_container);
errorText = header.findViewById(R.id.error_text);
resendButton = header.findViewById(R.id.resend_button);
sentDate = header.findViewById(R.id.sent_time);
receivedContainer = header.findViewById(R.id.received_container);
receivedDate = header.findViewById(R.id.received_time);
transport = header.findViewById(R.id.transport);
toFrom = header.findViewById(R.id.tofrom);
expiresContainer = header.findViewById(R.id.expires_container);
expiresInText = header.findViewById(R.id.expires_in);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
private void updateTransport(MessageRecord messageRecord) {
final String transportText;
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = getString(R.string.ConversationFragment_pending);
} else if (messageRecord.isPush()) {
transportText = getString(R.string.ConversationFragment_push);
} else if (messageRecord.isMms()) {
transportText = getString(R.string.ConversationFragment_mms);
} else {
transportText = getString(R.string.ConversationFragment_sms);
}
transport.setText(transportText);
}
private void updateTime(MessageRecord messageRecord) {
sentDate.setOnLongClickListener(null);
receivedDate.setOnLongClickListener(null);
if (messageRecord.isPending() || messageRecord.isFailed()) {
sentDate.setText("-");
receivedContainer.setVisibility(View.GONE);
} else {
Locale dateLocale = dynamicLanguage.getCurrentLocale();
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
sentDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
return true;
});
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
receivedDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
return true;
});
receivedContainer.setVisibility(View.VISIBLE);
} else {
receivedContainer.setVisibility(View.GONE);
}
}
}
private void updateExpirationTime(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
expiresContainer.setVisibility(View.GONE);
return;
}
expiresContainer.setVisibility(View.VISIBLE);
Util.runOnMain(new Runnable() {
@Override
public void run() {
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
long remaining = messageRecord.getExpiresIn() - elapsed;
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
expiresInText.setText(duration);
if (running) {
Util.runOnMainDelayed(this, 500);
}
}
});
}
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with;
} else if (messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__to;
} else {
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
Parcelable state = recipientsList.onSaveInstanceState();
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
recipientsList.onRestoreInstanceState(state);
}
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
if (conversationItem == null) {
if (messageRecord.isGroupAction()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
} else if (messageRecord.isOutgoing()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
} else {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
}
itemParent.addView(conversationItem);
}
}
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
return reader.getNext();
case MmsSmsDatabase.MMS_TRANSPORT:
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
return mmsReader.getNext();
default:
throw new AssertionError("no valid message type specified");
}
}
private void copyToClipboard(@NonNull String text) {
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
if (messageRecord == null) {
finish();
} else {
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
recipientsList.setAdapter(null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
@SuppressLint("StaticFieldLeak")
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
private final WeakReference<Context> weakContext;
private final MessageRecord messageRecord;
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
protected Context getContext() {
return weakContext.get();
}
@Override
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
Context context = getContext();
if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early");
return null;
}
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroup()) {
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1));
} else {
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
}
} else {
for (GroupReceiptInfo info : receiptInfoList) {
recipients.add(new RecipientDeliveryStatus(Recipient.resolved(info.getRecipientId()),
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
info.isUnidentified(),
info.getTimestamp()));
}
}
}
return recipients;
}
@Override
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return;
}
inflateMessageViewIfAbsent(messageRecord);
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.VISIBLE);
resendButton.setOnClickListener(this::onResendClicked);
metadataContainer.setVisibility(View.GONE);
} else if (messageRecord.isFailed()) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.GONE);
} else {
updateTransport(messageRecord);
updateTime(messageRecord);
updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.VISIBLE);
}
}
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
else return RecipientDeliveryStatus.Status.PENDING;
}
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
throw new AssertionError();
}
private void onResendClicked(View v) {
resendButton.setVisibility(View.GONE);
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
}
}
}

View File

@@ -1,112 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private final Context context;
private final GlideRequests glideRequests;
private final MessageRecord record;
private final List<RecipientDeliveryStatus> members;
private final boolean isPushGroup;
private final StableIdGenerator<RecipientId> idGenerator;
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
boolean isPushGroup)
{
this.context = context;
this.glideRequests = glideRequests;
this.record = record;
this.isPushGroup = isPushGroup;
this.members = members;
this.idGenerator = new StableIdGenerator<>();
}
@Override
public int getCount() {
return members.size();
}
@Override
public Object getItem(int position) {
return members.get(position);
}
@Override
public long getItemId(int position) {
return idGenerator.getId(members.get(position).recipient.getId());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false);
}
RecipientDeliveryStatus member = members.get(position);
((MessageRecipientListItem)convertView).set(glideRequests, record, member, isPushGroup);
return convertView;
}
@Override
public void onMovedToScrapHeap(View view) {
((MessageRecipientListItem)view).unbind();
}
static class RecipientDeliveryStatus {
enum Status {
UNKNOWN, PENDING, SENT, DELIVERED, READ
}
private final Recipient recipient;
private final Status deliveryStatus;
private final boolean isUnidentified;
private final long timestamp;
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
this.recipient = recipient;
this.deliveryStatus = deliveryStatus;
this.isUnidentified = isUnidentified;
this.timestamp = timestamp;
}
Status getDeliveryStatus() {
return deliveryStatus;
}
boolean isUnidentified() {
return isUnidentified;
}
public long getTimestamp() {
return timestamp;
}
public Recipient getRecipient() {
return recipient;
}
}
}

View File

@@ -1,200 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* A simple view to show the recipients of a message
*
* @author Jake McGinty
*/
public class MessageRecipientListItem extends RelativeLayout
implements RecipientForeverObserver
{
@SuppressWarnings("unused")
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
private RecipientDeliveryStatus member;
private GlideRequests glideRequests;
private FromTextView fromView;
private TextView errorDescription;
private TextView actionDescription;
private Button conflictButton;
private AvatarImageView contactPhotoImage;
private ImageView unidentifiedDeliveryIcon;
private DeliveryStatusView deliveryStatusView;
public MessageRecipientListItem(Context context) {
super(context);
}
public MessageRecipientListItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.fromView = findViewById(R.id.from);
this.errorDescription = findViewById(R.id.error_description);
this.actionDescription = findViewById(R.id.action_description);
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
this.conflictButton = findViewById(R.id.conflict_button);
this.unidentifiedDeliveryIcon = findViewById(R.id.ud_indicator);
this.deliveryStatusView = findViewById(R.id.delivery_status);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
observeMember();
}
@Override
protected void onDetachedFromWindow() {
unsubscribeFromMember();
super.onDetachedFromWindow();
}
public void set(final GlideRequests glideRequests,
final MessageRecord record,
final RecipientDeliveryStatus member,
final boolean isPushGroup)
{
unsubscribeFromMember();
this.glideRequests = glideRequests;
this.member = member;
observeMember();
fromView.setText(member.getRecipient());
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
setIssueIndicators(record, isPushGroup);
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
}
private void observeMember() {
if (isAttachedToWindow() && member != null && member.getRecipient() != null) {
member.getRecipient().live().observeForever(this);
}
}
private void unsubscribeFromMember() {
if (member != null && member.getRecipient() != null) member.getRecipient().live().removeForeverObserver(this);
}
private void setIssueIndicators(final MessageRecord record,
final boolean isPushGroup)
{
final NetworkFailure networkFailure = getNetworkFailure(record);
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
String errorText = "";
if (keyMismatch != null) {
conflictButton.setVisibility(View.VISIBLE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), record, keyMismatch).show());
} else if ((networkFailure != null && !record.isPending()) || (!isPushGroup && record.isFailed())) {
conflictButton.setVisibility(View.GONE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
} else {
if (record.isOutgoing()) {
if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) {
deliveryStatusView.setVisibility(View.GONE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) {
deliveryStatusView.setRead();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) {
deliveryStatusView.setDelivered();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) {
deliveryStatusView.setSent();
deliveryStatusView.setVisibility(View.VISIBLE);
}
} else {
deliveryStatusView.setVisibility(View.GONE);
}
conflictButton.setVisibility(View.GONE);
}
errorDescription.setText(errorText);
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
}
private NetworkFailure getNetworkFailure(final MessageRecord record) {
if (record.hasNetworkFailures()) {
for (final NetworkFailure failure : record.getNetworkFailures()) {
if (failure.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
return failure;
}
}
}
return null;
}
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
if (record.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
if (mismatch.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
return mismatch;
}
}
}
return null;
}
public void unbind() {
unsubscribeFromMember();
}
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
if (this.member != null && this.member.getRecipient().equals(recipient)) {
Log.d(TAG, "onRecipientChanged -- valid");
fromView.setText(recipient);
contactPhotoImage.setAvatar(glideRequests, recipient, false);
} else {
Log.d(TAG, "onRecipientChanged -- invalid");
}
}
}

View File

@@ -1,770 +0,0 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.loaders.RecipientMediaLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference;
import org.thoughtcrime.securesms.preferences.widgets.ContactPreference;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
@SuppressLint("StaticFieldLeak")
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>
{
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
public static final String RECIPIENT_ID = "recipient";
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
private static final String PREFERENCE_CALL_TONE = "pref_key_recipient_call_ringtone";
private static final String PREFERENCE_MESSAGE_VIBRATE = "pref_key_recipient_vibrate";
private static final String PREFERENCE_CALL_VIBRATE = "pref_key_recipient_call_vibrate";
private static final String PREFERENCE_BLOCK = "pref_key_recipient_block";
private static final String PREFERENCE_COLOR = "pref_key_recipient_color";
private static final String PREFERENCE_IDENTITY = "pref_key_recipient_identity";
private static final String PREFERENCE_ABOUT = "pref_key_number";
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private ImageView avatar;
private GlideRequests glideRequests;
private RecipientId recipientId;
private TextView threadPhotoRailLabel;
private ThreadPhotoRailView threadPhotoRailView;
private CollapsingToolbarLayout toolbarLayout;
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
public void onCreate(Bundle instanceState, boolean ready) {
setContentView(R.layout.recipient_preference_activity);
this.glideRequests = GlideApp.with(this);
this.recipientId = getIntent().getParcelableExtra(RECIPIENT_ID);
LiveRecipient recipient = Recipient.live(recipientId);
initializeToolbar();
setHeader(recipient.get());
recipient.observe(this, this::setHeader);
LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preference_fragment);
fragment.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return false;
}
private void initializeToolbar() {
this.toolbarLayout = findViewById(R.id.collapsing_toolbar);
this.avatar = findViewById(R.id.avatar);
this.threadPhotoRailView = findViewById(R.id.recent_photos);
this.threadPhotoRailLabel = findViewById(R.id.rail_label);
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.threadPhotoRailView.setListener(mediaRecord ->
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
mediaRecord,
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
SimpleTask.run(
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
(threadId) -> {
if (threadId == null) {
Log.i(TAG, "No thread id for recipient.");
} else {
this.threadPhotoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(this, threadId)));
}
}
);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setLogo(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().setStatusBarColor(Color.TRANSPARENT);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recipient_preference_root), (v, insets) -> {
ViewUtil.setTopMargin(toolbar, insets.getSystemWindowInsetTop());
return insets;
});
}
}
private void setHeader(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
glideRequests.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
avatar.setOnClickListener(null);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
avatar.setOnClickListener(v -> startActivity(AvatarPreviewActivity.intentFromRecipientId(RecipientPreferenceActivity.this, recipient.getId()),
AvatarPreviewActivity.createTransitionBundle(RecipientPreferenceActivity.this, avatar)));
return false;
}
})
.into(this.avatar);
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
this.toolbarLayout.setTitle(recipient.toShortString(this));
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new RecipientMediaLoader(this, recipientId, RecipientMediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
if (data != null && data.getCount() > 0) {
this.threadPhotoRailLabel.setVisibility(View.VISIBLE);
this.threadPhotoRailView.setVisibility(View.VISIBLE);
} else {
this.threadPhotoRailLabel.setVisibility(View.GONE);
this.threadPhotoRailView.setVisibility(View.GONE);
}
this.threadPhotoRailView.setCursor(glideRequests, data);
Bundle bundle = new Bundle();
bundle.putParcelable(RECIPIENT_ID, recipientId);
initFragment(R.id.preference_fragment, new RecipientPreferenceFragment(), null, bundle);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
this.threadPhotoRailView.setCursor(glideRequests, null);
}
public static class RecipientPreferenceFragment extends CorrectedPreferenceFragment {
private LiveRecipient recipient;
private boolean canHaveSafetyNumber;
@Override
public void onCreate(Bundle icicle) {
Log.i(TAG, "onCreate (fragment)");
super.onCreate(icicle);
initializeRecipients();
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
if (NotificationChannels.supported()) {
((SwitchPreferenceCompat) customNotificationsPref).setChecked(recipient.get().getNotificationChannel() != null);
customNotificationsPref.setOnPreferenceChangeListener(new CustomNotificationsChangedListener());
this.findPreference(PREFERENCE_MESSAGE_TONE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
this.findPreference(PREFERENCE_MESSAGE_VIBRATE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
if (recipient.get().getNotificationChannel() != null) {
final Context context = requireContext();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(getContext());
db.setMessageRingtone(recipient.getId(), NotificationChannels.getMessageRingtone(context, recipient.get()));
db.setMessageVibrate(recipient.getId(), NotificationChannels.getMessageVibrate(context, recipient.get()) ? VibrateState.ENABLED : VibrateState.DISABLED);
NotificationChannels.ensureCustomChannelConsistency(context);
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
} else {
customNotificationsPref.setVisible(false);
}
this.findPreference(PREFERENCE_MESSAGE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener(false));
this.findPreference(PREFERENCE_MESSAGE_TONE)
.setOnPreferenceClickListener(new RingtoneClickedListener(false));
this.findPreference(PREFERENCE_CALL_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener(true));
this.findPreference(PREFERENCE_CALL_TONE)
.setOnPreferenceClickListener(new RingtoneClickedListener(true));
this.findPreference(PREFERENCE_MESSAGE_VIBRATE)
.setOnPreferenceChangeListener(new VibrateChangeListener(false));
this.findPreference(PREFERENCE_CALL_VIBRATE)
.setOnPreferenceChangeListener(new VibrateChangeListener(true));
this.findPreference(PREFERENCE_MUTED)
.setOnPreferenceClickListener(new MuteClickedListener());
this.findPreference(PREFERENCE_BLOCK)
.setOnPreferenceClickListener(new BlockClickedListener());
this.findPreference(PREFERENCE_COLOR)
.setOnPreferenceChangeListener(new ColorChangeListener());
((ContactPreference)this.findPreference(PREFERENCE_ABOUT))
.setListener(new AboutNumberClickedListener());
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
Log.i(TAG, "onCreatePreferences...");
addPreferencesFromResource(R.xml.recipient_preferences);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onResume() {
super.onResume();
setSummaries(recipient.get());
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
findPreference(PREFERENCE_MESSAGE_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_MESSAGE_TONE), uri);
} else if (requestCode == 2 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
findPreference(PREFERENCE_CALL_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_CALL_TONE), uri);
}
}
@Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
recyclerView.setItemAnimator(null);
recyclerView.setLayoutAnimation(null);
return recyclerView;
}
private void initializeRecipients() {
this.recipient = Recipient.live(getArguments().getParcelable(RECIPIENT_ID));
this.recipient.observe(this, this::setSummaries);
}
private void setSummaries(Recipient recipient) {
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
Preference customPreference = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
Preference ringtoneMessagePreference = this.findPreference(PREFERENCE_MESSAGE_TONE);
Preference ringtoneCallPreference = this.findPreference(PREFERENCE_CALL_TONE);
ListPreference vibrateMessagePreference = (ListPreference) this.findPreference(PREFERENCE_MESSAGE_VIBRATE);
ListPreference vibrateCallPreference = (ListPreference) this.findPreference(PREFERENCE_CALL_VIBRATE);
ColorPickerPreference colorPreference = (ColorPickerPreference) this.findPreference(PREFERENCE_COLOR);
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
PreferenceCategory callCategory = (PreferenceCategory)this.findPreference("call_settings");
PreferenceCategory aboutCategory = (PreferenceCategory)this.findPreference("about");
PreferenceCategory aboutDivider = (PreferenceCategory)this.findPreference("about_divider");
ContactPreference aboutPreference = (ContactPreference)this.findPreference(PREFERENCE_ABOUT);
PreferenceCategory privacyCategory = (PreferenceCategory) this.findPreference("privacy_settings");
PreferenceCategory divider = (PreferenceCategory) this.findPreference("divider");
mutePreference.setChecked(recipient.isMuted());
ringtoneMessagePreference.setSummary(ringtoneMessagePreference.isEnabled() ? getRingtoneSummary(getContext(), recipient.getMessageRingtone()) : "");
ringtoneCallPreference.setSummary(getRingtoneSummary(getContext(), recipient.getCallRingtone()));
Pair<String, Integer> vibrateMessageSummary = getVibrateSummary(getContext(), recipient.getMessageVibrate());
Pair<String, Integer> vibrateCallSummary = getVibrateSummary(getContext(), recipient.getCallVibrate());
vibrateMessagePreference.setSummary(vibrateMessagePreference.isEnabled() ? vibrateMessageSummary.first : "");
vibrateMessagePreference.setValueIndex(vibrateMessageSummary.second);
vibrateCallPreference.setSummary(vibrateCallSummary.first);
vibrateCallPreference.setValueIndex(vibrateCallSummary.second);
blockPreference.setVisible(RecipientUtil.isBlockable(recipient));
if (recipient.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
if (recipient.isLocalNumber()) {
mutePreference.setVisible(false);
customPreference.setVisible(false);
ringtoneMessagePreference.setVisible(false);
vibrateMessagePreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (aboutCategory != null) aboutCategory.setVisible(false);
if (aboutDivider != null) aboutDivider.setVisible(false);
if (privacyCategory != null) privacyCategory.setVisible(false);
if (divider != null) divider.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);
}
if (recipient.isGroup()) {
if (colorPreference != null) colorPreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);
if (aboutCategory != null) aboutCategory.setVisible(false);
if (aboutDivider != null) aboutDivider.setVisible(false);
if (divider != null) divider.setVisible(false);
} else {
colorPreference.setColors(MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireActivity()));
colorPreference.setColor(recipient.getColor().toActionBarColor(requireActivity()));
if (FeatureFlags.profileDisplay()) {
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
} else {
aboutPreference.setTitle(formatRecipient(recipient));
aboutPreference.setSummary(recipient.getCustomLabel());
}
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
public void onSuccess(Optional<IdentityRecord> result) {
if (result.isPresent()) {
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
if (identityPreference != null) identityPreference.setEnabled(true);
} else if (canHaveSafetyNumber) {
if (identityPreference != null) identityPreference.setSummary(R.string.RecipientPreferenceActivity_available_once_a_message_has_been_sent_or_received);
if (identityPreference != null) identityPreference.setEnabled(false);
} else {
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
}
}
@Override
public void onFailure(ExecutionException e) {
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
}
});
}
if (recipient.isMmsGroup() && privacyCategory != null) {
privacyCategory.setVisible(false);
}
}
private @NonNull String formatRecipient(@NonNull Recipient recipient) {
if (recipient.getE164().isPresent()) return PhoneNumberUtils.formatNumber(recipient.requireE164());
else if (recipient.getEmail().isPresent()) return recipient.requireEmail();
else return "";
}
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
if (ringtone == null) {
return context.getString(R.string.preferences__default);
} else if (ringtone.toString().isEmpty()) {
return context.getString(R.string.preferences__silent);
} else {
Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone);
if (tone != null) {
return tone.getTitle(context);
}
}
return context.getString(R.string.preferences__default);
}
private @NonNull Pair<String, Integer> getVibrateSummary(@NonNull Context context, @NonNull VibrateState vibrateState) {
if (vibrateState == VibrateState.DEFAULT) {
return new Pair<>(context.getString(R.string.preferences__default), 0);
} else if (vibrateState == VibrateState.ENABLED) {
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_enabled), 1);
} else {
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_disabled), 2);
}
}
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
private final boolean calls;
RingtoneChangeListener(boolean calls) {
this.calls = calls;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = preference.getContext();
Uri value = (Uri)newValue;
Uri defaultValue;
if (calls) defaultValue = TextSecurePreferences.getCallNotificationRingtone(context);
else defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (defaultValue.equals(value)) value = null;
else if (value == null) value = Uri.EMPTY;
new AsyncTask<Uri, Void, Void>() {
@Override
protected Void doInBackground(Uri... params) {
if (calls) {
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipient.getId(), params[0]);
} else {
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), params[0]);
NotificationChannels.updateMessageRingtone(context, recipient.get(), params[0]);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, value);
return false;
}
}
private class RingtoneClickedListener implements Preference.OnPreferenceClickListener {
private final boolean calls;
RingtoneClickedListener(boolean calls) {
this.calls = calls;
}
@Override
public boolean onPreferenceClick(Preference preference) {
Uri current;
Uri defaultUri;
if (calls) {
current = recipient.get().getCallRingtone();
defaultUri = TextSecurePreferences.getCallNotificationRingtone(getContext());
} else {
current = recipient.get().getMessageRingtone();
defaultUri = TextSecurePreferences.getNotificationRingtone(getContext());
}
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
else if (current.toString().isEmpty()) current = null;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, calls ? RingtoneManager.TYPE_RINGTONE : RingtoneManager.TYPE_NOTIFICATION);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
startActivityForResult(intent, calls ? 2 : 1);
return true;
}
}
private class VibrateChangeListener implements Preference.OnPreferenceChangeListener {
private final boolean call;
VibrateChangeListener(boolean call) {
this.call = call;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int value = Integer.parseInt((String) newValue);
final VibrateState vibrateState = VibrateState.fromId(value);
final Context context = preference.getContext();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (call) {
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipient.getId(), vibrateState);
}
else {
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.getId(), vibrateState);
NotificationChannels.updateMessageVibrate(context, recipient.get(), vibrateState);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
return false;
}
}
private class ColorChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = getContext();
if (context == null) return true;
final int value = (Integer) newValue;
final MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, value);
final MaterialColor currentColor = recipient.get().getColor();
if (selectedColor == null) return true;
if (preference.isEnabled() && !currentColor.equals(selectedColor)) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(context).setColor(recipient.getId(), selectedColor);
if (recipient.get().resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(recipient.getId()));
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return true;
}
}
private class MuteClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (recipient.get().isMuted()) handleUnmute(preference.getContext());
else handleMute(preference.getContext());
return true;
}
private void handleMute(@NonNull Context context) {
MuteDialog.show(context, until -> setMuted(context, recipient.get(), until));
setSummaries(recipient.get());
}
private void handleUnmute(@NonNull Context context) {
setMuted(context, recipient.get(), 0);
}
private void setMuted(@NonNull final Context context, final Recipient recipient, final long until) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(context)
.setMuted(recipient.getId(), until);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
private final IdentityRecord identityKey;
private IdentityClickedListener(IdentityRecord identityKey) {
Log.i(TAG, "Identity record: " + identityKey);
this.identityKey = identityKey;
}
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
return true;
}
}
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Context context = preference.getContext();
if (recipient.get().isBlocked()) {
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
} else {
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
}
return true;
}
}
private class AboutNumberClickedListener implements ContactPreference.Listener {
@Override
public void onMessageClicked() {
CommunicationActions.startConversation(getContext(), recipient.get(), null);
}
@Override
public void onSecureCallClicked() {
CommunicationActions.startVoiceCall(getActivity(), recipient.get());
}
@Override
public void onSecureVideoClicked() {
CommunicationActions.startVideoCall(getActivity(), recipient.get());
}
@Override
public void onInSecureCallClicked() {
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
}
@Override
public void onLongClick() {
if (recipient.get().hasE164()) {
Util.copyToClipboard(requireContext(), recipient.get().requireE164());
ServiceUtil.getVibrator(requireContext()).vibrate(250);
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
}
private class CustomNotificationsChangedListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = preference.getContext();
final boolean enabled = (boolean) newValue;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (enabled) {
String channel = NotificationChannels.createChannelFor(context, recipient.get());
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), channel);
} else {
NotificationChannels.deleteChannelFor(context, recipient.get());
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), null);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
return true;
}
}
}
}

View File

@@ -486,7 +486,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString(getContext()))));
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setMovementMethod(LinkMovementMethod.getInstance());
}

View File

@@ -528,9 +528,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
callScreen.setLocalRenderer(event.getLocalRenderer());
callScreen.setRemoteRenderer(event.getRemoteRenderer());
viewModel.updateFromWebRtcViewModel(event);
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
viewModel.updateFromWebRtcViewModel(event, enableVideo);
if (enableVideo) {
enableVideoIfAvailable = false;
handleSetMuteVideo(false);
}

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@@ -40,7 +41,6 @@ import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -96,7 +96,9 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
if (table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMessage, null, count);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
@@ -283,11 +285,15 @@ public class FullBackupExporter extends FullBackupBase {
return result;
}
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
}
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
String where = MmsDatabase.ID + " = ?";

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,7 +54,7 @@ public class MaterialColors {
return null;
}
public int[] asConversationColorArray(@NonNull Context context) {
public @ColorInt int[] asConversationColorArray(@NonNull Context context) {
int[] results = new int[colors.size()];
int index = 0;

View File

@@ -16,7 +16,6 @@ import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
@@ -26,8 +25,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Objects;
@@ -113,6 +112,9 @@ public final class AvatarImageView extends AppCompatImageView {
this.fallbackPhotoProvider = fallbackPhotoProvider;
}
/**
* Shows self as the actual profile picture.
*/
public void setRecipient(@NonNull Recipient recipient) {
if (recipient.isLocalNumber()) {
setAvatar(GlideApp.with(this), null, false);
@@ -122,6 +124,13 @@ public final class AvatarImageView extends AppCompatImageView {
}
}
/**
* Shows self as the note to self icon.
*/
public void setAvatar(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
@@ -165,7 +174,7 @@ public final class AvatarImageView extends AppCompatImageView {
if (quickContactEnabled) {
super.setOnClickListener(v -> {
Context context = getContext();
if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) {
if (recipient.isPushGroup()) {
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
ManageGroupActivity.createTransitionBundle(context, this));
} else {
@@ -173,7 +182,8 @@ public final class AvatarImageView extends AppCompatImageView {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
} else {
context.startActivity(RecipientPreferenceActivity.getLaunchIntent(context, recipient.getId()));
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
ManageRecipientActivity.createTransitionBundle(context, this));
}
}
});

View File

@@ -42,7 +42,7 @@ public class FromTextView extends EmojiTextView {
}
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
String fromString = recipient.toShortString(getContext());
String fromString = recipient.getDisplayName(getContext());
int typeface;
@@ -61,19 +61,6 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self));
} else if (!FeatureFlags.profileDisplay() && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL){
builder.append(profileName);
builder.append(fromSpan);
} else {
builder.append(fromSpan);
builder.append(profileName);
}
} else {
builder.append(fromSpan);
}

View File

@@ -189,7 +189,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
authorView.setText(author.isLocalNumber() ? getContext().getString(R.string.QuoteView_you)
: author.toShortString(getContext()));
: author.getDisplayName(getContext()));
// We use the raw color resource because Android 4.x was struggling with tints here
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));

View File

@@ -35,6 +35,7 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private boolean showVideoForOutgoing = false;
private long callConnectedTime = -1;
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
@@ -97,7 +98,7 @@ public class WebRtcCallViewModel extends ViewModel {
}
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel) {
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
@@ -106,6 +107,13 @@ public class WebRtcCallViewModel extends ViewModel {
}
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
if (enableVideo) {
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
showVideoForOutgoing = false;
}
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getLocalCameraState().isEnabled(),
@@ -177,8 +185,8 @@ public class WebRtcCallViewModel extends ViewModel {
}
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
if (shouldDisplayLocalVideo) return state;
else return WebRtcLocalRenderState.GONE;
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean neverDisplayControls, @NonNull WebRtcControls controls) {

View File

@@ -274,7 +274,7 @@ public class ContactsCursorLoader extends CursorLoader {
String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() : recipient.getE164().or(recipient.getEmail()).or("");
recentConversations.addRow(new Object[] { recipient.getId().serialize(),
recipient.toShortString(getContext()),
recipient.getDisplayName(getContext()),
stringId,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",

View File

@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.contacts.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
@@ -18,7 +16,16 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
public final class FallbackPhoto80dp implements FallbackContactPhoto {
@DrawableRes private final int drawable80dp;
private final MaterialColor backgroundColor;
public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull MaterialColor backgroundColor) {
this.drawable80dp = drawable80dp;
this.backgroundColor = backgroundColor;
}
@Override
public Drawable asDrawable(Context context, int color) {
return buildDrawable(context);
@@ -40,13 +47,13 @@ public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
}
private @NonNull Drawable buildDrawable(@NonNull Context context) {
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable)));
Drawable foreground = AppCompatResources.getDrawable(context, R.drawable.ic_group_80);
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
int foregroundInset = ViewUtil.dpToPx(24);
DrawableCompat.setTint(background, MaterialColor.ULTRAMARINE.toAvatarColor(context));
DrawableCompat.setTint(background, backgroundColor.toAvatarColor(context));
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);

View File

@@ -82,14 +82,12 @@ import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.GroupMembersDialog;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PromptMmsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
@@ -207,6 +205,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -225,7 +224,6 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
@@ -495,7 +493,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(glideRequests, recipientSnapshot);
setActionBarColor(recipientSnapshot.getColor());
setBlockedUserState(recipientSnapshot, isSecureText, isDefaultSms);
setGroupShareProfileReminder(recipientSnapshot);
calculateCharactersRemaining();
if (recipientSnapshot.getGroupId().isPresent() && recipientSnapshot.getGroupId().get().isV2()) {
@@ -757,10 +754,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
}
} else if (isActiveV2Group || isActiveGroup && FeatureFlags.newGroupUI()) {
inflater.inflate(R.menu.conversation_push_group_v2_options, menu);
} else if (isActiveGroup) {
inflater.inflate(R.menu.conversation_push_group_options, menu);
inflater.inflate(R.menu.conversation_active_group_options, menu);
} else if (isActiveV2Group || isActiveGroup) {
inflater.inflate(R.menu.conversation_active_group_options, menu);
}
}
@@ -803,9 +799,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
hideMenuItem(menu, R.id.menu_mute_notifications);
}
if (FeatureFlags.newGroupUI()) {
hideMenuItem(menu, R.id.menu_group_recipients);
}
hideMenuItem(menu, R.id.menu_group_recipients);
if (isActiveV2Group) {
hideMenuItem(menu, R.id.menu_mute_notifications);
@@ -884,8 +878,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
case R.id.menu_edit_group: handleEditPushGroupV1(); return true;
case R.id.menu_group_settings: handleManagePushGroup(); return true;
case R.id.menu_group_settings: handleManageGroup(); return true;
case R.id.menu_leave: handleLeavePushGroup(); return true;
case R.id.menu_invite: handleInviteLink(); return true;
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
@@ -893,7 +886,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case android.R.id.home: onBackPressed(); return true;
case android.R.id.home: onNavigateUp(); return true;
}
return false;
@@ -1042,14 +1035,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleConversationSettings() {
if (FeatureFlags.newGroupUI() && isPushGroupConversation()) {
handleManagePushGroup();
if (isGroupConversation()) {
handleManageGroup();
return;
}
if (isInMessageRequest()) return;
Intent intent = RecipientPreferenceActivity.getLaunchIntent(this, recipient.getId());
Intent intent = ManageRecipientActivity.newIntentFromConversation(this, recipient.getId());
startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
}
@@ -1206,12 +1199,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
null);
}
private void handleEditPushGroupV1() {
startActivityForResult(GroupCreateActivity.newEditGroupIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV1()), GROUP_EDIT);
}
private void handleManagePushGroup() {
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requirePush()),
private void handleManageGroup() {
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()),
GROUP_EDIT,
ManageGroupActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image)));
}
@@ -1965,7 +1954,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setVerified(identityRecords.isVerified());
setBlockedUserState(recipient, isSecureText, isDefaultSms);
setActionBarColor(recipient.getColor());
setGroupShareProfileReminder(recipient);
updateReminders();
updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId());
initializeSecurity(isSecureText, isDefaultSms);
@@ -2147,12 +2135,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
if (recipient.isBlocked() && !FeatureFlags.messageRequests()) {
unblockButton.setVisibility(View.VISIBLE);
inputPanel.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);
} else if (!isSecureText && isPushGroupConversation()) {
if (!isSecureText && isPushGroupConversation()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE);
@@ -2170,19 +2153,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
if (FeatureFlags.messageRequests()) {
return;
}
if (recipient.isPushGroup() && !recipient.isProfileSharing()) {
groupShareProfileView.get().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) {
groupShareProfileView.get().setVisibility(View.GONE);
}
}
private void calculateCharactersRemaining() {
String messageBody = composeText.getTextTrimmed();
TransportOption transportOption = sendButton.getSelectedTransport();
@@ -2396,10 +2366,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long id = fragment.stageOutgoingMessage(message);
SimpleTask.run(() -> {
if (!FeatureFlags.messageRequests() && initiating) {
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
}
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), threadId, () -> fragment.releaseOutgoingMessage(id));
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
@@ -2470,10 +2436,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage);
SimpleTask.run(() -> {
if (!FeatureFlags.messageRequests() && initiating) {
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
}
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
}, result -> {
sendComplete(result);
@@ -2517,10 +2479,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
if (!FeatureFlags.messageRequests() && initiating) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
}
return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
}
@@ -3150,7 +3108,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
String[] unverifiedNames = new String[unverifiedIdentities.size()];
for (int i=0;i<unverifiedIdentities.size();i++) {
unverifiedNames[i] = Recipient.resolved(unverifiedIdentities.get(i).getRecipientId()).toShortString(ConversationActivity.this);
unverifiedNames[i] = Recipient.resolved(unverifiedIdentities.get(i).getRecipientId()).getDisplayName(ConversationActivity.this);
}
AlertDialog.Builder builder = new AlertDialog.Builder(ConversationActivity.this);

View File

@@ -479,7 +479,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
return headerView != null;
}
private boolean hasFooter() {
public boolean hasFooter() {
return footerView != null;
}
@@ -510,6 +510,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
}
}
public @Nullable MessageRecord getLastVisibleMessageRecord(int position) {
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
}
static class ConversationViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
super(itemView);

View File

@@ -7,26 +7,32 @@ final class ConversationData {
private final long threadId;
private final long lastSeen;
private final int lastSeenPosition;
private final int lastScrolledPosition;
private final boolean hasSent;
private final boolean isMessageRequestAccepted;
private final boolean hasPreMessageRequestMessages;
private final int jumpToPosition;
private final int threadSize;
ConversationData(long threadId,
long lastSeen,
int lastSeenPosition,
int lastScrolledPosition,
boolean hasSent,
boolean isMessageRequestAccepted,
boolean hasPreMessageRequestMessages,
int jumpToPosition)
int jumpToPosition,
int threadSize)
{
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.hasSent = hasSent;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
this.jumpToPosition = jumpToPosition;
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.lastScrolledPosition = lastScrolledPosition;
this.hasSent = hasSent;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
this.jumpToPosition = jumpToPosition;
this.threadSize = threadSize;
}
public long getThreadId() {
@@ -41,6 +47,10 @@ final class ConversationData {
return lastSeenPosition;
}
int getLastScrolledPosition() {
return lastScrolledPosition;
}
boolean hasSent() {
return hasSent;
}
@@ -57,7 +67,15 @@ final class ConversationData {
return jumpToPosition >= 0;
}
boolean shouldScrollToLastSeen() {
return lastSeenPosition > 0;
}
int getJumpToPosition() {
return jumpToPosition;
}
int getThreadSize() {
return threadSize;
}
}

View File

@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.List;
@@ -30,16 +32,13 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
private final Context context;
private final long threadId;
private final DataUpdatedCallback dataUpdateCallback;
private ConversationDataSource(@NonNull Context context,
long threadId,
@NonNull Invalidator invalidator,
@NonNull DataUpdatedCallback dataUpdateCallback)
@NonNull Invalidator invalidator)
{
this.context = context;
this.threadId = threadId;
this.dataUpdateCallback = dataUpdateCallback;
ContentObserver contentObserver = new ContentObserver(null) {
@Override
@@ -66,10 +65,6 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
int totalCount = db.getConversationCount(threadId);
int effectiveCount = params.requestedStartPosition;
if (totalCount == 0 || params.requestedStartPosition > totalCount) {
}
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
@@ -79,13 +74,12 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
if (!isInvalid()) {
SizeFixResult result = ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.messages, params.requestedStartPosition, result.total);
Util.runOnMain(dataUpdateCallback::onDataUpdated);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
}
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + (isInvalid() ? " -- invalidated" : ""));
}
@Override
@@ -104,59 +98,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
callback.onResult(records);
if (!isInvalid()) {
Util.runOnMain(dataUpdateCallback::onDataUpdated);
}
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}
private static @NonNull SizeFixResult ensureMultipleOfPageSize(@NonNull List<MessageRecord> records,
int startPosition,
int pageSize,
int total)
{
if (records.size() + startPosition == total || (records.size() != 0 && records.size() % pageSize == 0)) {
return new SizeFixResult(records, total);
}
if (records.size() < pageSize) {
Log.w(TAG, "Hit a miscalculation where we don't have the full dataset, but it's smaller than a page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
return new SizeFixResult(records, records.size() + startPosition);
}
Log.w(TAG, "Hit a miscalculation where our data size isn't a multiple of the page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
int overflow = records.size() % pageSize;
return new SizeFixResult(records.subList(0, records.size() - overflow), total);
}
private static class SizeFixResult {
final List<MessageRecord> messages;
final int total;
private SizeFixResult(@NonNull List<MessageRecord> messages, int total) {
this.messages = messages;
this.total = total;
}
}
interface DataUpdatedCallback {
void onDataUpdated();
}
static class Invalidator {
private Runnable callback;
synchronized void invalidate() {
if (callback != null) {
callback.run();
}
}
private synchronized void observe(@NonNull Runnable callback) {
this.callback = callback;
}
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
}
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
@@ -164,18 +106,16 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
private final Context context;
private final long threadId;
private final Invalidator invalidator;
private final DataUpdatedCallback callback;
Factory(Context context, long threadId, @NonNull Invalidator invalidator, @NonNull DataUpdatedCallback callback) {
Factory(Context context, long threadId, @NonNull Invalidator invalidator) {
this.context = context;
this.threadId = threadId;
this.invalidator = invalidator;
this.callback = callback;
}
@Override
public @NonNull DataSource<Integer, MessageRecord> create() {
return new ConversationDataSource(context, threadId, invalidator, callback);
return new ConversationDataSource(context, threadId, invalidator);
}
}
}

View File

@@ -21,7 +21,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -61,7 +60,6 @@ import com.annimon.stream.Stream;
import com.google.android.collect.Sets;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -75,7 +73,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickList
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -88,6 +85,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -113,6 +111,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -163,8 +162,7 @@ public class ConversationFragment extends Fragment {
private ConversationBannerView emptyConversationBanner;
private MessageRequestViewModel messageRequestViewModel;
private ConversationViewModel conversationViewModel;
private Deferred deferred = new Deferred();
private SnapToTopDataObserver snapToTopDataObserver;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
@@ -198,12 +196,11 @@ public class ConversationFragment extends Fragment {
list.setLayoutManager(layoutManager);
list.setItemAnimator(null);
if (FeatureFlags.messageRequests()) {
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
}
snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
initializeLoadMoreView(topLoadMoreView);
initializeLoadMoreView(bottomLoadMoreView);
@@ -226,16 +223,12 @@ public class ConversationFragment extends Fragment {
Log.i(TAG, "submitList skipped an invalid list");
}
});
conversationViewModel.getConversationMetadata().observe(this, data -> deferred.defer(() -> presentConversationMetadata(data)));
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
return view;
}
private void setupListLayoutListeners() {
if (!FeatureFlags.messageRequests()) {
return;
}
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
list.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@@ -284,6 +277,23 @@ public class ConversationFragment extends Fragment {
initializeTypingObserver();
}
@Override
public void onPause() {
super.onPause();
int lastVisiblePosition = getListLayoutManager().findLastVisibleItemPosition();
int firstVisiblePosition = getListLayoutManager().findFirstCompletelyVisibleItemPosition();
final long lastVisibleMessageTimestamp;
if (firstVisiblePosition != 0 && lastVisiblePosition != RecyclerView.NO_POSITION) {
MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition);
lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0;
} else {
lastVisibleMessageTimestamp = 0;
}
SignalExecutors.BOUNDED.submit(() -> DatabaseFactory.getThreadDatabase(requireContext()).setLastScrolled(threadId, lastVisibleMessageTimestamp));
}
@Override
public void onStop() {
super.onStop();
@@ -312,7 +322,7 @@ public class ConversationFragment extends Fragment {
}
int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition());
scrollToLastSeenPosition(position);
snapToTopDataObserver.requestScrollPosition(position);
}
private void initializeMessageRequestViewModel() {
@@ -406,7 +416,6 @@ public class ConversationFragment extends Fragment {
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
deferred.setDeferred(true);
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
@@ -425,12 +434,12 @@ public class ConversationFragment extends Fragment {
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(new DataObserver());
adapter.registerAdapterDataObserver(snapToTopDataObserver);
setLastSeen(conversationViewModel.getLastSeen());
emptyConversationBanner.setVisibility(View.GONE);
} else if (FeatureFlags.messageRequests() && threadId == -1) {
} else if (threadId == -1) {
emptyConversationBanner.setVisibility(View.VISIBLE);
}
}
@@ -546,7 +555,7 @@ public class ConversationFragment extends Fragment {
this.threadId = threadId;
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
deferred.setDeferred(true);
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(threadId, -1);
initializeListAdapter();
}
@@ -698,13 +707,7 @@ public class ConversationFragment extends Fragment {
private void handleDisplayDetails(MessageRecord message) {
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
intent.putExtra(MessageDetailsActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, recipient.get().isGroup() && message.isPush());
startActivity(intent);
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId));
}
private void handleForwardMessage(MessageRecord message) {
@@ -866,47 +869,49 @@ public class ConversationFragment extends Fragment {
return;
}
if (FeatureFlags.messageRequests()) {
adapter.setFooterView(conversationBanner);
} else {
adapter.setFooterView(null);
}
adapter.setFooterView(conversationBanner);
setLastSeen(conversation.getLastSeen());
if (FeatureFlags.messageRequests() && !conversation.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
} else {
clearHeaderIfNotTyping(adapter);
Runnable afterScroll = () -> {
if (!conversation.isMessageRequestAccepted()) {
snapToTopDataObserver.requestScrollPosition(adapter.getItemCount() - 1);
}
}
listener.onCursorChanged();
setLastSeen(conversation.getLastSeen());
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
if (!conversation.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
} else {
clearHeaderIfNotTyping(adapter);
}
}
if (conversation.shouldJumpToMessage()) {
scrollToStartingPosition(conversation.getJumpToPosition());
listener.onCursorChanged();
};
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
int lastScrolledPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastScrolledPosition());
if (conversation.getThreadSize() == 0) {
afterScroll.run();
} else if (conversation.shouldJumpToMessage()) {
snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition())
.withOnScrollRequestComplete(() -> {
afterScroll.run();
getListAdapter().pulseHighlightItem(conversation.getJumpToPosition());
})
.submit();
} else if (conversation.isMessageRequestAccepted()) {
scrollToLastSeenPosition(lastSeenPosition);
} else if (FeatureFlags.messageRequests()) {
list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
}
}
private void scrollToStartingPosition(int startingPosition) {
list.post(() -> {
list.getLayoutManager().scrollToPosition(startingPosition);
getListAdapter().pulseHighlightItem(startingPosition);
});
}
private void scrollToLastSeenPosition(int lastSeenPosition) {
if (lastSeenPosition > 0) {
list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
.withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, list.getHeight()))
.withOnScrollRequestComplete(afterScroll)
.submit();
} else {
snapToTopDataObserver.buildScrollPosition(adapter.getItemCount() - 1)
.withOnScrollRequestComplete(afterScroll)
.submit();
}
}
@@ -941,24 +946,21 @@ public class ConversationFragment extends Fragment {
}
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
if (position >= 0) {
list.scrollToPosition(position);
if (getListAdapter() == null || getListAdapter().getItem(position) == null) {
Log.i(TAG, "[moveToMessagePosition] Position " + position + " not currently populated. Scheduling a jump.");
conversationViewModel.scheduleForNextMessageUpdate(() -> {
list.scrollToPosition(position);
getListAdapter().pulseHighlightItem(position);
});
} else {
getListAdapter().pulseHighlightItem(position);
}
} else {
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
if (onMessageNotFound != null) {
onMessageNotFound.run();
}
}
conversationViewModel.onConversationDataAvailable(threadId, position);
snapToTopDataObserver.buildScrollPosition(position)
.withOnPerformScroll(((layoutManager, p) ->
list.post(() -> {
layoutManager.scrollToPosition(p);
getListAdapter().pulseHighlightItem(position);
})
))
.withOnInvalidPosition(() -> {
if (onMessageNotFound != null) {
onMessageNotFound.run();
}
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
})
.submit();
}
private void maybeShowSwipeToReplyTooltip() {
@@ -1058,44 +1060,6 @@ public class ConversationFragment extends Fragment {
}
}
private class DataObserver extends RecyclerView.AdapterDataObserver {
private final Rect rect = new Rect();
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (deferred.isDeferred()) {
deferred.setDeferred(false);
return;
}
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
return;
}
if (list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
int firstVisibleItem = getListLayoutManager().findFirstVisibleItemPosition();
if (firstVisibleItem == 0) {
View view = getListLayoutManager().findViewByPosition(0);
if (view == null) {
return;
}
view.getDrawingRect(rect);
list.offsetDescendantRectToMyCoords(view, rect);
int bottom = rect.bottom;
list.getDrawingRect(rect);
if (bottom <= rect.bottom) {
getListLayoutManager().scrollToPosition(0);
}
}
}
}
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
@@ -1303,6 +1267,52 @@ public class ConversationFragment extends Fragment {
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
private final class ConversationSnapToTopDataObserver extends SnapToTopDataObserver {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
@Nullable ScrollRequestValidator scrollRequestValidator)
{
super(recyclerView, scrollRequestValidator);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
return;
}
super.onItemRangeInserted(positionStart, itemCount);
}
}
private final class ConversationScrollRequestValidator implements SnapToTopDataObserver.ScrollRequestValidator {
@Override
public boolean isPositionStillValid(int position) {
if (getListAdapter() == null) {
return position >= 0;
} else {
return position >= 0 && position < getListAdapter().getItemCount();
}
}
@Override
public boolean isItemAtPositionLoaded(int position) {
if (getListAdapter() == null) {
return false;
} else if (getListAdapter().hasFooter() && position == getListAdapter().getItemCount() - 1) {
return true;
} else {
return getListAdapter().getItem(position) != null;
}
}
}
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
private final MessageRecord messageRecord;
@@ -1449,33 +1459,4 @@ public class ConversationFragment extends Fragment {
}
}
private static class Deferred {
private Runnable deferred;
private boolean isDeferred;
public void defer(@Nullable Runnable deferred) {
this.deferred = deferred;
executeIfNecessary();
}
public void setDeferred(boolean isDeferred) {
this.isDeferred = isDeferred;
executeIfNecessary();
}
public boolean isDeferred() {
return isDeferred;
}
private void executeIfNecessary() {
if (deferred != null && !isDeferred) {
Runnable local = deferred;
deferred = null;
local.run();
}
}
}
}

View File

@@ -58,7 +58,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.AlertView;
@@ -77,7 +76,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@@ -92,6 +90,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
@@ -109,7 +108,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil;
@@ -1002,21 +1000,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@SuppressLint("SetTextI18n")
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
if (FeatureFlags.profileDisplay()) {
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
} else {
groupSender.setText(recipient.toShortString(context));
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
groupSenderProfileName.setVisibility(View.VISIBLE);
} else {
groupSenderProfileName.setText(null);
groupSenderProfileName.setVisibility(View.GONE);
}
}
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
}
}
@@ -1390,13 +1375,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (!shouldInterceptClicks(messageRecord) && parent != null) {
parent.onClick(v);
} else if (messageRecord.isFailed()) {
Intent intent = new Intent(context, MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread && messageRecord.isPush());
intent.putExtra(MessageDetailsActivity.RECIPIENT_EXTRA, conversationRecipient.getId());
context.startActivity(intent);
context.startActivity(MessageDetailsActivity.getIntentForMessageDetails(context, messageRecord, conversationRecipient.getId(), messageRecord.getThreadId()));
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
handleApproveIdentity();
} else if (messageRecord.isPendingInsecureSmsFallback()) {

View File

@@ -7,10 +7,10 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.Pair;
import java.util.concurrent.Executor;
@@ -35,23 +35,30 @@ class ConversationRepository {
}
private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
Pair<Long, Boolean> lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
long lastSeen = lastSeenAndHasSent.first();
boolean hasSent = lastSeenAndHasSent.second();
int lastSeenPosition = 0;
long lastSeen = metadata.getLastSeen();
boolean hasSent = metadata.hasSent();
int lastSeenPosition = 0;
long lastScrolled = metadata.getLastScrolled();
int lastScrolledPosition = 0;
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
boolean hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
if (lastSeen > 0) {
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionForLastSeen(threadId, lastSeen);
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
}
if (lastSeenPosition <= 0) {
lastSeen = 0;
}
return new ConversationData(threadId, lastSeen, lastSeenPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition);
if (lastSeen == 0 && lastScrolled > 0) {
lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled);
}
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition, threadSize);
}
}

View File

@@ -113,16 +113,9 @@ public class ConversationTitleView extends RelativeLayout {
}
private void setRecipientTitle(Recipient recipient) {
if (FeatureFlags.profileDisplay()) {
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else setIndividualRecipientTitle(recipient);
} else {
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else if (TextUtils.isEmpty(recipient.getName(getContext()))) setNonContactRecipientTitle(recipient);
else setContactRecipientTitle(recipient);
}
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else setIndividualRecipientTitle(recipient);
}
@SuppressLint("SetTextI18n")
@@ -138,25 +131,8 @@ public class ConversationTitleView extends RelativeLayout {
updateSubtitleVisibility();
}
private void setContactRecipientTitle(Recipient recipient) {
this.title.setText(recipient.getName(getContext()));
if (TextUtils.isEmpty(recipient.getCustomLabel())) {
this.subtitle.setText(null);
} else {
this.subtitle.setText(recipient.getCustomLabel());
}
updateSubtitleVisibility();
}
private void setGroupRecipientTitle(Recipient recipient) {
if (FeatureFlags.profileDisplay()) {
this.title.setText(recipient.getDisplayName(getContext()));
} else {
this.title.setText(recipient.getName(getContext()));
}
this.title.setText(recipient.getDisplayName(getContext()));
this.subtitle.setText(Stream.of(recipient.getParticipants())
.sorted((a, b) -> Boolean.compare(a.isLocalNumber(), b.isLocalNumber()))
.map(r -> r.isLocalNumber() ? getResources().getString(R.string.ConversationTitleView_you)

View File

@@ -3,11 +3,8 @@ package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
@@ -15,19 +12,17 @@ import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.thoughtcrime.securesms.conversation.ConversationDataSource.Invalidator;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.whispersystems.libsignal.util.Pair;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Objects;
class ConversationViewModel extends ViewModel {
@@ -40,7 +35,6 @@ class ConversationViewModel extends ViewModel {
private final MutableLiveData<Long> threadId;
private final LiveData<PagedList<MessageRecord>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final List<Runnable> onNextMessageLoad;
private final Invalidator invalidator;
private int jumpToPosition;
@@ -51,28 +45,35 @@ class ConversationViewModel extends ViewModel {
this.conversationRepository = new ConversationRepository();
this.recentMedia = new MutableLiveData<>();
this.threadId = new MutableLiveData<>();
this.onNextMessageLoad = new CopyOnWriteArrayList<>();
this.invalidator = new Invalidator();
LiveData<ConversationData> conversationDataForRequestedThreadId = Transformations.switchMap(threadId, thread -> {
return conversationRepository.getConversationData(thread, jumpToPosition);
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
jumpToPosition = -1;
return conversationData;
});
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(conversationDataForRequestedThreadId, data -> {
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator, this::onMessagesUpdated);
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25)
.build();
final int startPosition;
if (jumpToPosition > 0) {
startPosition = jumpToPosition;
} else {
if (data.shouldJumpToMessage()) {
startPosition = data.getJumpToPosition();
} else if (data.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
startPosition = data.getLastSeenPosition();
} else if (data.isMessageRequestAccepted()) {
startPosition = data.getLastScrolledPosition();
} else {
startPosition = data.getThreadSize();
}
Log.d(TAG, "Starting at position " + startPosition + " :: " + jumpToPosition + " :: " + data.getLastSeenPosition());
Log.d(TAG, "Starting at position startPosition: " + startPosition + " jumpToPosition: " + jumpToPosition + " lastSeenPosition: " + data.getLastSeenPosition() + " lastScrolledPosition: " + data.getLastScrolledPosition());
return Transformations.map(new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationDataSource.EXECUTOR)
.setInitialLoadKey(Math.max(startPosition, 0))
@@ -82,13 +83,11 @@ class ConversationViewModel extends ViewModel {
this.messages = Transformations.map(messagesForThreadId, Pair::second);
LiveData<Long> threadIdForLoadedMessages = Transformations.distinctUntilChanged(Transformations.map(messagesForThreadId, Pair::first));
LiveData<DistinctConversationDataByThreadId> distinctData = LiveDataUtil.combineLatest(messagesForThreadId,
metadata,
(m, data) -> new DistinctConversationDataByThreadId(data));
conversationMetadata = Transformations.switchMap(threadIdForLoadedMessages, m -> {
LiveData<ConversationData> data = conversationRepository.getConversationData(m, jumpToPosition);
jumpToPosition = -1;
return data;
});
conversationMetadata = Transformations.map(Transformations.distinctUntilChanged(distinctData), DistinctConversationDataByThreadId::getConversationData);
}
void onAttachmentKeyboardOpen() {
@@ -122,24 +121,12 @@ class ConversationViewModel extends ViewModel {
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeenPosition() : 0;
}
void scheduleForNextMessageUpdate(@NonNull Runnable runnable) {
onNextMessageLoad.add(runnable);
}
@Override
protected void onCleared() {
super.onCleared();
invalidator.invalidate();
}
private void onMessagesUpdated() {
for (Runnable runnable : onNextMessageLoad) {
runnable.run();
}
onNextMessageLoad.clear();
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
@@ -147,4 +134,29 @@ class ConversationViewModel extends ViewModel {
return modelClass.cast(new ConversationViewModel());
}
}
private static class DistinctConversationDataByThreadId {
private final ConversationData conversationData;
private DistinctConversationDataByThreadId(@NonNull ConversationData conversationData) {
this.conversationData = conversationData;
}
public @NonNull ConversationData getConversationData() {
return conversationData;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DistinctConversationDataByThreadId that = (DistinctConversationDataByThreadId) o;
return Objects.equals(conversationData.getThreadId(), that.conversationData.getThreadId());
}
@Override
public int hashCode() {
return Objects.hash(conversationData.getThreadId());
}
}
}

View File

@@ -1,184 +1,210 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A CursorAdapter for building a list of conversation threads.
*
* @author Moxie Marlinspike
*/
class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationListAdapter.ViewHolder> {
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1;
private static final int MESSAGE_TYPE_THREAD = 2;
private static final int MESSAGE_TYPE_INBOX_ZERO = 3;
private static final int TYPE_THREAD = 1;
private static final int TYPE_ACTION = 2;
private static final int TYPE_PLACEHOLDER = 3;
private final @NonNull ThreadDatabase threadDatabase;
private final @NonNull GlideRequests glideRequests;
private final @NonNull Locale locale;
private final @NonNull LayoutInflater inflater;
private final @Nullable ItemClickListener clickListener;
private final @NonNull MessageDigest digest;
private enum Payload {
TYPING_INDICATOR,
SELECTION
}
private final Map<Long, ThreadRecord> batchSet = Collections.synchronizedMap(new HashMap<>());
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private final GlideRequests glideRequests;
private final OnConversationClickListener onConversationClickListener;
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new HashMap<>());
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private int archived;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
{
super(itemView);
}
protected ConversationListAdapter(@NonNull GlideRequests glideRequests, @NonNull OnConversationClickListener onConversationClickListener) {
super(new ConversationDiffCallback());
public BindableConversationListItem getItem() {
return (BindableConversationListItem)itemView;
}
this.glideRequests = glideRequests;
this.onConversationClickListener = onConversationClickListener;
}
@Override
public long getItemId(@NonNull Cursor cursor) {
ThreadRecord record = getThreadRecord(cursor);
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == TYPE_ACTION) {
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.conversation_list_item_action, parent, false));
return Conversions.byteArrayToLong(digest.digest(record.getRecipient().getId().serialize().getBytes()));
}
holder.itemView.setOnClickListener(v -> {
int position = holder.getAdapterPosition();
@Override
protected long getFastAccessItemId(int position) {
return super.getFastAccessItemId(position);
}
ConversationListAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable Cursor cursor,
@Nullable ItemClickListener clickListener)
{
super(context, cursor);
try {
this.glideRequests = glideRequests;
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
this.locale = locale;
this.inflater = LayoutInflater.from(context);
this.clickListener = clickListener;
this.digest = MessageDigest.getInstance("SHA1");
setHasStableIds(true);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError("SHA-1 missing");
}
}
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
if (viewType == MESSAGE_TYPE_SWITCH_ARCHIVE) {
ConversationListItemAction action = (ConversationListItemAction) inflater.inflate(R.layout.conversation_list_item_action,
parent, false);
action.setOnClickListener(v -> {
if (clickListener != null) clickListener.onSwitchToArchive();
if (position != RecyclerView.NO_POSITION) {
onConversationClickListener.onShowArchiveClick();
}
});
return new ViewHolder(action);
} else if (viewType == MESSAGE_TYPE_INBOX_ZERO) {
return new ViewHolder((ConversationListItemInboxZero)inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false));
return holder;
} else if (viewType == TYPE_THREAD) {
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
.inflate(R.layout.conversation_list_item_view, parent, false));
holder.itemView.setOnClickListener(v -> {
int position = holder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
onConversationClickListener.onConversationClick(getItem(position));
}
});
holder.itemView.setOnLongClickListener(v -> {
int position = holder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
return onConversationClickListener.onConversationLongClick(getItem(position));
}
return false;
});
return holder;
} else if (viewType == TYPE_PLACEHOLDER) {
View v = new FrameLayout(parent.getContext());
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
return new PlaceholderViewHolder(v);
} else {
final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
parent, false);
item.setOnClickListener(view -> {
if (clickListener != null) clickListener.onItemClick(item);
});
item.setOnLongClickListener(view -> {
if (clickListener != null) clickListener.onItemLongClick(item);
return true;
});
return new ViewHolder(item);
throw new IllegalStateException("Unknown type! " + viewType);
}
}
@Override
public void onItemViewRecycled(ViewHolder holder) {
holder.getItem().unbind();
}
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet.keySet(), batchMode);
}
@Override
public int getItemViewType(@NonNull Cursor cursor) {
ThreadRecord threadRecord = getThreadRecord(cursor);
if (threadRecord.getDistributionType() == ThreadDatabase.DistributionTypes.ARCHIVE) {
return MESSAGE_TYPE_SWITCH_ARCHIVE;
} else if (threadRecord.getDistributionType() == ThreadDatabase.DistributionTypes.INBOX_ZERO) {
return MESSAGE_TYPE_INBOX_ZERO;
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
return MESSAGE_TYPE_THREAD;
for (Object payloadObject : payloads) {
if (payloadObject instanceof Payload) {
Payload payload = (Payload) payloadObject;
if (payload == Payload.SELECTION) {
((ConversationViewHolder) holder).getConversationListItem().setBatchMode(batchMode);
} else {
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
}
}
}
}
}
public void setTypingThreads(@NonNull Set<Long> threadsIds) {
typingSet.clear();
typingSet.addAll(threadsIds);
notifyDataSetChanged();
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder.getItemViewType() == TYPE_ACTION) {
ConversationViewHolder casted = (ConversationViewHolder) holder;
casted.getConversationListItem().bind(new ThreadRecord.Builder(100)
.setBody("")
.setDate(100)
.setRecipient(Recipient.UNKNOWN)
.setCount(archived)
.build(),
glideRequests,
Locale.getDefault(),
typingSet,
getBatchSelectionIds(),
batchMode);
} else if (holder.getItemViewType() == TYPE_THREAD) {
ConversationViewHolder casted = (ConversationViewHolder) holder;
Conversation conversation = Objects.requireNonNull(getItem(position));
casted.getConversationListItem().bind(conversation.getThreadRecord(),
glideRequests,
conversation.getLocale(),
typingSet,
getBatchSelectionIds(),
batchMode);
}
}
private ThreadRecord getThreadRecord(@NonNull Cursor cursor) {
return threadDatabase.readerFor(cursor).getCurrent();
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof ConversationViewHolder) {
((ConversationViewHolder) holder).getConversationListItem().unbind();
}
}
void toggleThreadInBatchSet(@NonNull ThreadRecord thread) {
if (batchSet.containsKey(thread.getThreadId())) {
batchSet.remove(thread.getThreadId());
} else if (thread.getThreadId() != -1) {
batchSet.put(thread.getThreadId(), thread);
void setTypingThreads(@NonNull Set<Long> typingThreadSet) {
this.typingSet.clear();
this.typingSet.addAll(typingThreadSet);
notifyItemRangeChanged(0, getItemCount(), Payload.TYPING_INDICATOR);
}
void toggleConversationInBatchSet(@NonNull Conversation conversation) {
if (batchSet.containsKey(conversation.getThreadRecord().getThreadId())) {
batchSet.remove(conversation.getThreadRecord().getThreadId());
} else if (conversation.getThreadRecord().getThreadId() != -1) {
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
}
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
}
Collection<Conversation> getBatchSelection() {
return batchSet.values();
}
void updateArchived(int archived) {
int oldArchived = this.archived;
this.archived = archived;
if (oldArchived != archived) {
if (archived == 0) {
notifyItemRemoved(getItemCount());
} else if (oldArchived == 0) {
notifyItemInserted(getItemCount() - 1);
} else {
notifyItemChanged(getItemCount() - 1);
}
}
}
@Override
public int getItemCount() {
return (archived > 0 ? 1 : 0) + super.getItemCount();
}
@Override
public int getItemViewType(int position) {
if (archived > 0 && position == getItemCount() - 1) {
return TYPE_ACTION;
} else if (getItem(position) == null) {
return TYPE_PLACEHOLDER;
} else {
return TYPE_THREAD;
}
}
@@ -186,8 +212,15 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
return batchSet.keySet();
}
@NonNull Set<ThreadRecord> getBatchSelection() {
return new HashSet<>(batchSet.values());
void selectAllThreads() {
for (int i = 0; i < getItemCount(); i++) {
Conversation conversation = getItem(i);
if (conversation != null && conversation.getThreadRecord().getThreadId() != -1) {
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
}
}
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
}
void initializeBatchMode(boolean toggle) {
@@ -196,23 +229,48 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
}
private void unselectAllThreads() {
this.batchSet.clear();
this.notifyDataSetChanged();
batchSet.clear();
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
}
void selectAllThreads() {
for (int i = 0; i < getItemCount(); i++) {
ThreadRecord record = getThreadRecord(getCursorAtPositionOrThrow(i));
if (record.getThreadId() != -1) {
batchSet.put(record.getThreadId(), record);
}
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
private final BindableConversationListItem conversationListItem;
ConversationViewHolder(@NonNull View itemView) {
super(itemView);
conversationListItem = (BindableConversationListItem) itemView;
}
public BindableConversationListItem getConversationListItem() {
return conversationListItem;
}
this.notifyDataSetChanged();
}
interface ItemClickListener {
void onItemClick(ConversationListItem item);
void onItemLongClick(ConversationListItem item);
void onSwitchToArchive();
private static final class ConversationDiffCallback extends DiffUtil.ItemCallback<Conversation> {
@Override
public boolean areItemsTheSame(@NonNull Conversation oldItem, @NonNull Conversation newItem) {
return oldItem.getThreadRecord().getThreadId() == newItem.getThreadRecord().getThreadId();
}
@Override
public boolean areContentsTheSame(@NonNull Conversation oldItem, @NonNull Conversation newItem) {
return oldItem.equals(newItem);
}
}
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
PlaceholderViewHolder(@NonNull View itemView) {
super(itemView);
}
}
interface OnConversationClickListener {
void onConversationClick(Conversation conversation);
boolean onConversationLongClick(Conversation conversation);
void onShowArchiveClick();
}
}

View File

@@ -17,14 +17,9 @@
package org.thoughtcrime.securesms.conversationlist;
import android.annotation.SuppressLint;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.DrawableRes;
import androidx.annotation.MenuRes;
@@ -35,22 +30,17 @@ import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
public class ConversationListArchiveFragment extends ConversationListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, ActionMode.Callback, ItemClickListener
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
{
private RecyclerView list;
private View emptyState;
@@ -71,10 +61,10 @@ public class ConversationListArchiveFragment extends ConversationListFragment
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Toolbar toolbar = view.findViewById(R.id.toolbar_basic);
@@ -86,16 +76,14 @@ public class ConversationListArchiveFragment extends ConversationListFragment
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), null, true);
protected void onPostSubmitList() {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> arg0, Cursor cursor) {
super.onLoadFinished(arg0, cursor);
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
protected boolean isArchived() {
return true;
}
@Override

View File

@@ -0,0 +1,157 @@
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
private static final String TAG = Log.tag(ConversationListDataSource.class);
protected final ThreadDatabase threadDatabase;
protected ConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
ContentObserver contentObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
invalidate();
context.getContentResolver().unregisterContentObserver(this);
}
};
invalidator.observe(() -> {
invalidate();
context.getContentResolver().unregisterContentObserver(contentObserver);
});
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
}
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
else return new ArchivedConversationListDataSource(context, invalidator);
}
@Override
public final void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Conversation> callback) {
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
Locale locale = Locale.getDefault();
int totalCount = getTotalCount();
int effectiveCount = params.requestedStartPosition;
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
conversations.add(new Conversation(record, locale));
effectiveCount++;
}
}
if (!isInvalid()) {
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
}
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}
@Override
public final void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Conversation> callback) {
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.loadSize);
Locale locale = Locale.getDefault();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
conversations.add(new Conversation(record, locale));
}
}
callback.onResult(conversations);
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}
protected abstract int getTotalCount();
protected abstract Cursor getCursor(long offset, long limit);
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
ArchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
}
@Override
protected int getTotalCount() {
return threadDatabase.getArchivedConversationListCount();
}
@Override
protected Cursor getCursor(long offset, long limit) {
return threadDatabase.getArchivedConversationList(offset, limit);
}
}
private static class UnarchivedConversationListDataSource extends ConversationListDataSource {
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
}
@Override
protected int getTotalCount() {
return threadDatabase.getUnarchivedConversationListCount();
}
@Override
protected Cursor getCursor(long offset, long limit) {
return threadDatabase.getConversationList(offset, limit);
}
}
static class Factory extends DataSource.Factory<Integer, Conversation> {
private final Context context;
private final Invalidator invalidator;
private final boolean isArchived;
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
this.context = context;
this.invalidator = invalidator;
this.isArchived = isArchived;
}
@Override
public @NonNull DataSource<Integer, Conversation> create() {
return ConversationListDataSource.create(context, invalidator, isArchived);
}
}
}

View File

@@ -23,7 +23,6 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -59,8 +58,6 @@ import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -91,13 +88,12 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
@@ -119,6 +115,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -126,7 +123,6 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
@@ -138,9 +134,8 @@ import java.util.Set;
import static android.app.Activity.RESULT_OK;
public class ConversationListFragment extends MainFragment implements LoaderManager.LoaderCallbacks<Cursor>,
ActionMode.Callback,
ItemClickListener,
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
ConversationListAdapter.OnConversationClickListener,
ConversationListSearchAdapter.EventListener,
MainNavigator.BackHandler,
MegaphoneActionController
@@ -157,23 +152,24 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
R.drawable.empty_inbox_4,
R.drawable.empty_inbox_5 };
private ActionMode actionMode;
private RecyclerView list;
private ReminderView reminderView;
private View emptyState;
private ImageView emptyImage;
private TextView searchEmptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private SearchToolbar searchToolbar;
private ImageView searchAction;
private View toolbarShadow;
private ConversationListViewModel viewModel;
private RecyclerView.Adapter activeAdapter;
private ConversationListAdapter defaultAdapter;
private ConversationListSearchAdapter searchAdapter;
private StickyHeaderDecoration searchAdapterDecoration;
private ViewGroup megaphoneContainer;
private ActionMode actionMode;
private RecyclerView list;
private ReminderView reminderView;
private View emptyState;
private ImageView emptyImage;
private TextView searchEmptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private SearchToolbar searchToolbar;
private ImageView searchAction;
private View toolbarShadow;
private ConversationListViewModel viewModel;
private RecyclerView.Adapter activeAdapter;
private ConversationListAdapter defaultAdapter;
private ConversationListSearchAdapter searchAdapter;
private StickyHeaderDecoration searchAdapterDecoration;
private ViewGroup megaphoneContainer;
private SnapToTopDataObserver snapToTopDataObserver;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
@@ -214,10 +210,12 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
reminderView.setOnDismissListener(this::updateReminders);
list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
list.setItemAnimator(new DeleteItemAnimator());
list.addOnScrollListener(new ScrollListener());
snapToTopDataObserver = new SnapToTopDataObserver(list, null);
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
@@ -247,7 +245,6 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
super.onResume();
updateReminders();
list.getAdapter().notifyDataSetChanged();
EventBus.getDefault().register(this);
if (TextSecurePreferences.isSmsEnabled(requireContext())) {
@@ -257,9 +254,12 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
SimpleTask.run(getLifecycle(), Recipient::self, this::initializeProfileIcon);
if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
activeAdapter = defaultAdapter;
list.removeItemDecoration(searchAdapterDecoration);
list.setAdapter(defaultAdapter);
setAdapter(defaultAdapter);
}
if (activeAdapter != null) {
activeAdapter.notifyDataSetChanged();
}
}
@@ -314,9 +314,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
private boolean closeSearchIfOpen() {
if (searchToolbar.isVisible() || activeAdapter == searchAdapter) {
activeAdapter = defaultAdapter;
list.removeItemDecoration(searchAdapterDecoration);
list.setAdapter(defaultAdapter);
setAdapter(defaultAdapter);
searchToolbar.collapse();
return true;
}
@@ -356,6 +355,11 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
-1);
}
@Override
public void onShowArchiveClick() {
getNavigator().goToArchiveList();
}
@Override
public void onContactClicked(@NonNull Recipient contact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
@@ -440,16 +444,14 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
activeAdapter = searchAdapter;
list.setAdapter(searchAdapter);
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
activeAdapter = defaultAdapter;
list.removeItemDecoration(searchAdapterDecoration);
list.setAdapter(defaultAdapter);
setAdapter(defaultAdapter);
}
}
}
@@ -457,19 +459,36 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
list.setAdapter(defaultAdapter);
setAdapter(defaultAdapter);
}
});
}
private void initializeListAdapters() {
defaultAdapter = new ConversationListAdapter (requireContext(), GlideApp.with(this), Locale.getDefault(), null, this);
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault () );
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
activeAdapter = defaultAdapter;
list.setAdapter(defaultAdapter);
LoaderManager.getInstance(this).restartLoader(0, null, this);
setAdapter(defaultAdapter);
}
@SuppressWarnings("rawtypes")
private void setAdapter(@NonNull RecyclerView.Adapter adapter) {
RecyclerView.Adapter oldAdapter = activeAdapter;
activeAdapter = adapter;
if (oldAdapter == activeAdapter) {
return;
}
list.setAdapter(adapter);
if (adapter == defaultAdapter) {
defaultAdapter.registerAdapterDataObserver(snapToTopDataObserver);
} else {
defaultAdapter.unregisterAdapterDataObserver(snapToTopDataObserver);
}
}
private void initializeTypingObserver() {
@@ -482,11 +501,16 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
});
}
protected boolean isArchived() {
return false;
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory()).get(ConversationListViewModel.class);
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
viewModel.getSearchResult().observe(this, this::onSearchResultChanged);
viewModel.getMegaphone().observe(this, this::onMegaphoneChanged);
viewModel.getConversationList().observe(this, this::onSubmitList);
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
@Override
@@ -733,14 +757,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), null, false);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> arg0, Cursor cursor) {
if (cursor == null || cursor.getCount() <= 0) {
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
if (conversationList.isEmpty()) {
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.VISIBLE);
emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
@@ -753,45 +771,39 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
cameraFab.stopPulse();
}
defaultAdapter.changeCursor(cursor);
defaultAdapter.submitList(conversationList.getConversations());
defaultAdapter.updateArchived(conversationList.getArchivedCount());
onPostSubmitList();
}
protected void onPostSubmitList() {
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> arg0) {
defaultAdapter.changeCursor(null);
}
@Override
public void onItemClick(ConversationListItem item) {
public void onConversationClick(Conversation conversation) {
if (actionMode == null) {
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType());
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
} else {
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
adapter.toggleThreadInBatchSet(item.getThread());
defaultAdapter.toggleConversationInBatchSet(conversation);
if (adapter.getBatchSelectionIds().size() == 0) {
if (defaultAdapter.getBatchSelectionIds().size() == 0) {
actionMode.finish();
} else {
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
setCorrectMenuVisibility(actionMode.getMenu());
}
adapter.notifyDataSetChanged();
}
}
@Override
public void onItemLongClick(ConversationListItem item) {
public boolean onConversationLongClick(Conversation conversation) {
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
defaultAdapter.initializeBatchMode(true);
defaultAdapter.toggleThreadInBatchSet(item.getThread());
defaultAdapter.notifyDataSetChanged();
}
defaultAdapter.toggleConversationInBatchSet(conversation);
@Override
public void onSwitchToArchive() {
getNavigator().goToArchiveList();
return true;
}
@Override
@@ -870,7 +882,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
}
private void setCorrectMenuVisibility(@NonNull Menu menu) {
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(thread -> !thread.isRead());
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
if (hasUnread) {
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);

View File

@@ -174,28 +174,19 @@ public class ConversationListItem extends RelativeLayout
this.fromView.setText(recipient.get(), thread.isRead());
}
if (typingThreads.contains(threadId)) {
this.subjectView.setVisibility(INVISIBLE);
updateTypingIndicator(typingThreads);
this.typingView.setVisibility(VISIBLE);
this.typingView.startAnimation();
} else {
this.typingView.setVisibility(GONE);
this.typingView.stopAnimation();
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
this.subjectView.setVisibility(VISIBLE);
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
if (thread.getGroupAddedBy() != null) {
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
groupAddedBy.observeForever(groupAddedByObserver);
}
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
if (thread.getGroupAddedBy() != null) {
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
groupAddedBy.observeForever(groupAddedByObserver);
}
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
if (thread.getDate() > 0) {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
@@ -291,11 +282,27 @@ public class ConversationListItem extends RelativeLayout
}
}
private void setBatchMode(boolean batchMode) {
@Override
public void setBatchMode(boolean batchMode) {
this.batchMode = batchMode;
setSelected(batchMode && selectedThreads.contains(thread.getThreadId()));
}
@Override
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
if (typingThreads.contains(threadId)) {
this.subjectView.setVisibility(INVISIBLE);
this.typingView.setVisibility(VISIBLE);
this.typingView.startAnimation();
} else {
this.typingView.setVisibility(GONE);
this.typingView.stopAnimation();
this.subjectView.setVisibility(VISIBLE);
}
}
public Recipient getRecipient() {
return recipient.get();
}
@@ -421,7 +428,7 @@ public class ConversationListItem extends RelativeLayout
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().toShortString(context)));
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().getDisplayName(context)));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
int seconds = (int)(thread.getExpiresIn() / 1000);
if (seconds <= 0) {
@@ -433,7 +440,7 @@ public class ConversationListItem extends RelativeLayout
if (thread.getRecipient().isGroup()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
} else {
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().toShortString(context)));
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().getDisplayName(context)));
}
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));

View File

@@ -55,4 +55,14 @@ public class ConversationListItemAction extends LinearLayout implements Bindable
public void unbind() {
}
@Override
public void setBatchMode(boolean batchMode) {
}
@Override
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
}
}

View File

@@ -49,4 +49,14 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
{
}
@Override
public void setBatchMode(boolean batchMode) {
}
@Override
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
}
}

View File

@@ -10,9 +10,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
@@ -20,36 +25,67 @@ import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
class ConversationListViewModel extends ViewModel {
private final Application application;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final MutableLiveData<Integer> archivedCount;
private final LiveData<ConversationList> conversationList;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final ContentObserver observer;
private final Invalidator invalidator;
private String lastQuery;
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository) {
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
this.application = application;
this.megaphone = new MutableLiveData<>();
this.searchResult = new MutableLiveData<>();
this.archivedCount = new MutableLiveData<>();
this.searchRepository = searchRepository;
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
this.debouncer = new Debouncer(300);
this.invalidator = new Invalidator();
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
if (!TextUtils.isEmpty(getLastQuery())) {
searchRepository.query(getLastQuery(), searchResult::postValue);
}
if (!isArchived) {
updateArchivedCount();
}
}
};
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15)
.setInitialLoadSizeHint(30)
.setEnablePlaceholders(true)
.build();
LiveData<PagedList<Conversation>> conversationList = new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
.setInitialLoadKey(0)
.build();
if (isArchived) {
this.archivedCount.setValue(0);
} else {
updateArchivedCount();
}
application.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, observer);
this.conversationList = LiveDataUtil.combineLatest(conversationList, this.archivedCount, ConversationList::new);
}
@NonNull LiveData<SearchResult> getSearchResult() {
@@ -60,6 +96,10 @@ class ConversationListViewModel extends ViewModel {
return megaphone;
}
@NonNull LiveData<ConversationList> getConversationList() {
return conversationList;
}
void onVisible() {
megaphoneRepository.getNextMegaphone(megaphone::postValue);
}
@@ -95,15 +135,51 @@ class ConversationListViewModel extends ViewModel {
@Override
protected void onCleared() {
invalidator.invalidate();
debouncer.clear();
application.getContentResolver().unregisterContentObserver(observer);
}
private void updateArchivedCount() {
SignalExecutors.BOUNDED.execute(() -> {
archivedCount.postValue(DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount());
});
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
private final boolean isArchived;
public Factory(boolean isArchived) {
this.isArchived = isArchived;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository()));
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
}
}
final static class ConversationList {
private final PagedList<Conversation> conversations;
private final int archivedCount;
ConversationList(PagedList<Conversation> conversations, int archivedCount) {
this.conversations = conversations;
this.archivedCount = archivedCount;
}
PagedList<Conversation> getConversations() {
return conversations;
}
int getArchivedCount() {
return archivedCount;
}
boolean isEmpty() {
return conversations.isEmpty() && archivedCount == 0;
}
}
}

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.conversationlist.model;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import java.util.Locale;
import java.util.Objects;
public class Conversation {
private final ThreadRecord threadRecord;
private final Locale locale;
public Conversation(@NonNull ThreadRecord threadRecord, @NonNull Locale locale) {
this.threadRecord = threadRecord;
this.locale = locale;
}
public @NonNull ThreadRecord getThreadRecord() {
return threadRecord;
}
public @NonNull Locale getLocale() {
return locale;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Conversation that = (Conversation) o;
return threadRecord.equals(that.threadRecord) &&
locale.equals(that.locale);
}
@Override
public int hashCode() {
return Objects.hash(threadRecord, locale);
}
}

View File

@@ -215,13 +215,15 @@ public final class GroupDatabase extends Database {
}
}
public List<String> getPushGroupNamesContainingMember(RecipientId recipientId) {
@WorkerThread
public List<String> getPushGroupNamesContainingMember(@NonNull RecipientId recipientId) {
return Stream.of(getPushGroupsContainingMember(recipientId))
.map(GroupRecord::getTitle)
.map(groupRecord -> Recipient.resolved(groupRecord.getRecipientId()).getDisplayName(context))
.toList();
}
public List<GroupRecord> getPushGroupsContainingMember(RecipientId recipientId) {
@WorkerThread
public @NonNull List<GroupRecord> getPushGroupsContainingMember(@NonNull RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
String query = MEMBERS + " LIKE ? AND " + MMS + " = ?";

View File

@@ -599,11 +599,13 @@ public class MmsDatabase extends MessagingDatabase {
db.beginTransaction();
try {
String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)";
for (long id : ids) {
ContentValues contentValues = new ContentValues();
contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{String.valueOf(id)});
db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)});
if (threadId < 0) {
threadId = getThreadIdForMessage(id);

View File

@@ -124,9 +124,9 @@ public class MmsSmsDatabase extends Database {
return new Pair<>(id, latestQuit);
}
public int getMessagePositionForLastSeen(long threadId, long lastSeen) {
public int getMessagePositionOnOrAfterTimestamp(long threadId, long timestamp) {
String[] projection = new String[] { "COUNT(*)" };
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + lastSeen;
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " >= " + timestamp;
try (Cursor cursor = queryTables(projection, selection, null, null)) {
if (cursor != null && cursor.moveToNext()) {

View File

@@ -3,10 +3,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import net.sqlcipher.database.SQLiteDatabase;
@@ -16,31 +14,38 @@ import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.util.UUID;
public class PushDatabase extends Database {
private static final String TAG = PushDatabase.class.getSimpleName();
private static final String TABLE_NAME = "push";
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String SOURCE_E164 = "source";
public static final String SOURCE_UUID = "source_uuid";
public static final String DEVICE_ID = "device_id";
public static final String LEGACY_MSG = "body";
public static final String CONTENT = "content";
public static final String TIMESTAMP = "timestamp";
public static final String SERVER_TIMESTAMP = "server_timestamp";
public static final String SERVER_GUID = "server_guid";
private static final String TABLE_NAME = "push";
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String SOURCE_E164 = "source";
public static final String SOURCE_UUID = "source_uuid";
public static final String DEVICE_ID = "device_id";
public static final String LEGACY_MSG = "body";
public static final String CONTENT = "content";
public static final String TIMESTAMP = "timestamp";
public static final String SERVER_RECEIVED_TIMESTAMP = "server_timestamp";
public static final String SERVER_DELIVERED_TIMESTAMP = "server_delivered_timestamp";
public static final String SERVER_GUID = "server_guid";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
TYPE + " INTEGER, " + SOURCE_E164 + " TEXT, " + SOURCE_UUID + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER, " +
SERVER_TIMESTAMP + " INTEGER DEFAULT 0, " + SERVER_GUID + " TEXT DEFAULT NULL);";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
TYPE + " INTEGER, " +
SOURCE_E164 + " TEXT, " +
SOURCE_UUID + " TEXT, " +
DEVICE_ID + " INTEGER, " +
LEGACY_MSG + " TEXT, " +
CONTENT + " TEXT, " +
TIMESTAMP + " INTEGER, " +
SERVER_RECEIVED_TIMESTAMP + " INTEGER DEFAULT 0, " +
SERVER_DELIVERED_TIMESTAMP + " INTEGER DEFAULT 0, " +
SERVER_GUID + " TEXT DEFAULT NULL);";
public PushDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
@@ -60,7 +65,8 @@ public class PushDatabase extends Database {
values.put(LEGACY_MSG, envelope.hasLegacyMessage() ? Base64.encodeBytes(envelope.getLegacyMessage()) : "");
values.put(CONTENT, envelope.hasContent() ? Base64.encodeBytes(envelope.getContent()) : "");
values.put(TIMESTAMP, envelope.getTimestamp());
values.put(SERVER_TIMESTAMP, envelope.getServerTimestamp());
values.put(SERVER_RECEIVED_TIMESTAMP, envelope.getServerReceivedTimestamp());
values.put(SERVER_DELIVERED_TIMESTAMP, envelope.getServerDeliveredTimestamp());
values.put(SERVER_GUID, envelope.getUuid());
return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
@@ -87,7 +93,8 @@ public class PushDatabase extends Database {
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)),
Util.isEmpty(legacyMessage) ? null : Base64.decode(legacyMessage),
Util.isEmpty(content) ? null : Base64.decode(content),
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP)),
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)),
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)),
cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)));
}
} catch (IOException e) {
@@ -154,15 +161,16 @@ public class PushDatabase extends Database {
if (cursor == null || !cursor.moveToNext())
return null;
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID));
String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164));
int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID));
String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG));
String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
long serverTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP));
String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID));
String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164));
int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID));
String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG));
String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
long serverReceivedTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP));
long serverDeliveredTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP));
String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID));
return new SignalServiceEnvelope(type,
SignalServiceAddress.fromRaw(sourceUuid, sourceE164),
@@ -170,7 +178,8 @@ public class PushDatabase extends Database {
timestamp,
legacyMessage != null ? Base64.decode(legacyMessage) : null,
content != null ? Base64.decode(content) : null,
serverTimestamp,
serverReceivedTimestamp,
serverDeliveredTimestamp,
serverGuid);
} catch (IOException e) {
throw new AssertionError(e);

View File

@@ -19,9 +19,11 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
@@ -59,6 +61,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -101,6 +104,7 @@ public class RecipientDatabase extends Database {
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String LAST_PROFILE_FETCH = "last_profile_fetch";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String UUID_CAPABILITY = "uuid_supported";
@@ -122,7 +126,8 @@ public class RecipientDatabase extends Database {
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH,
NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
@@ -165,6 +170,10 @@ public class RecipientDatabase extends Database {
public static VibrateState fromId(int id) {
return values()[id];
}
public static VibrateState fromBoolean(boolean enabled) {
return enabled ? ENABLED : DISABLED;
}
}
public enum RegisteredState {
@@ -294,6 +303,7 @@ public class RecipientDatabase extends Database {
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " +
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
@@ -518,10 +528,11 @@ public class RecipientDatabase extends Database {
try {
for (SignalContactRecord insert : contactInserts) {
ContentValues values = validateContactValuesForInsert(getValuesForStorageContact(insert));
ContentValues values = validateContactValuesForInsert(getValuesForStorageContact(insert, true));
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
if (id < 0) {
values = validateContactValuesForInsert(getValuesForStorageContact(insert, false));
Log.w(TAG, "Failed to insert! It's likely that these were newly-registered users that were missed in the merge. Doing an update instead.");
if (insert.getAddress().getNumber().isPresent()) {
@@ -550,7 +561,7 @@ public class RecipientDatabase extends Database {
}
for (RecordUpdate<SignalContactRecord> update : contactUpdates) {
ContentValues values = getValuesForStorageContact(update.getNew());
ContentValues values = getValuesForStorageContact(update.getNew(), false);
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
@@ -708,7 +719,7 @@ public class RecipientDatabase extends Database {
}
}
private static @NonNull ContentValues getValuesForStorageContact(@NonNull SignalContactRecord contact) {
private static @NonNull ContentValues getValuesForStorageContact(@NonNull SignalContactRecord contact, boolean isInsert) {
ContentValues values = new ContentValues();
if (contact.getAddress().getUuid().isPresent()) {
@@ -728,6 +739,11 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, contact.isBlocked() ? "1" : "0");
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
if (contact.isProfileSharingEnabled() && isInsert) {
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
}
return values;
}
@@ -835,6 +851,7 @@ public class RecipientDatabase extends Database {
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
@@ -901,7 +918,7 @@ public class RecipientDatabase extends Database {
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection,
Recipient.Capability.deserialize(uuidCapabilityValue),
@@ -930,6 +947,23 @@ public class RecipientDatabase extends Database {
}
}
public void setColorIfNotSet(@NonNull RecipientId id, @NonNull MaterialColor color) {
if (setColorIfNotSetInternal(id, color)) {
Recipient.live(id).refresh();
}
}
private boolean setColorIfNotSetInternal(@NonNull RecipientId id, @NonNull MaterialColor color) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String query = ID + " = ? AND " + COLOR + " IS NULL";
String[] args = new String[]{ id.serialize() };
ContentValues values = new ContentValues();
values.put(COLOR, color.serialize());
return db.update(TABLE_NAME, values, query, args) > 0;
}
public void setDefaultSubscriptionId(@NonNull RecipientId id, int defaultSubscriptionId) {
ContentValues values = new ContentValues();
values.put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId);
@@ -1208,7 +1242,11 @@ public class RecipientDatabase extends Database {
public void setProfileSharing(@NonNull RecipientId id, @SuppressWarnings("SameParameterValue") boolean enabled) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
if (update(id, contentValues)) {
boolean profiledUpdated = update(id, contentValues);
boolean colorUpdated = enabled && setColorIfNotSetInternal(id, ContactColors.generateFor(Recipient.resolved(id).getDisplayName(context)));
if (profiledUpdated || colorUpdated) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
StorageSyncHelper.scheduleSyncForDataChange();
@@ -1559,6 +1597,53 @@ public class RecipientDatabase extends Database {
return recipients;
}
/**
* @param lastInteractionThreshold Only include contacts that have been interacted with since this time.
* @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time.
* @param limit Only return at most this many contact.
*/
public List<RecipientId> getRecipientsForRoutineProfileFetch(long lastInteractionThreshold, long lastProfileFetchThreshold, int limit) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Set<Recipient> recipientsWithinInteractionThreshold = new LinkedHashSet<>();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && record.getDate() > lastInteractionThreshold) {
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
if (recipient.isGroup()) {
recipientsWithinInteractionThreshold.addAll(recipient.getParticipants());
} else {
recipientsWithinInteractionThreshold.add(recipient);
}
}
}
return Stream.of(recipientsWithinInteractionThreshold)
.filterNot(Recipient::isLocalNumber)
.filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold)
.limit(limit)
.map(Recipient::getId)
.toList();
}
public void markProfilesFetched(@NonNull Collection<RecipientId> ids, long time) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
ContentValues values = new ContentValues(1);
values.put(LAST_PROFILE_FETCH, time);
for (RecipientId id : ids) {
db.update(TABLE_NAME, values, ID_WHERE, new String[] { id.serialize() });
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
List<String> blockedE164 = Stream.of(blocked)
.filter(b -> b.getNumber().isPresent())
@@ -1772,7 +1857,10 @@ public class RecipientDatabase extends Database {
refreshQualifyingValues.put(SYSTEM_PHONE_TYPE, systemPhoneType);
refreshQualifyingValues.put(SYSTEM_CONTACT_URI, systemContactUri);
if (update(id, refreshQualifyingValues)) {
boolean updatedValues = update(id, refreshQualifyingValues);
boolean updatedColor = displayName != null && setColorIfNotSetInternal(id, ContactColors.generateFor(displayName));
if (updatedValues || updatedColor) {
pendingContactInfoMap.put(id, new PendingContactInfo(displayName, photoUri, systemPhoneLabel, systemContactUri));
}
@@ -1854,6 +1942,7 @@ public class RecipientDatabase extends Database {
private final String signalProfileAvatar;
private final boolean hasProfileImage;
private final boolean profileSharing;
private final long lastProfileFetch;
private final String notificationChannel;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
@@ -1892,6 +1981,7 @@ public class RecipientDatabase extends Database {
@Nullable String signalProfileAvatar,
boolean hasProfileImage,
boolean profileSharing,
long lastProfileFetch,
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection,
@@ -1930,6 +2020,7 @@ public class RecipientDatabase extends Database {
this.signalProfileAvatar = signalProfileAvatar;
this.hasProfileImage = hasProfileImage;
this.profileSharing = profileSharing;
this.lastProfileFetch = lastProfileFetch;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
@@ -2060,6 +2151,10 @@ public class RecipientDatabase extends Database {
return profileSharing;
}
public long getLastProfileFetch() {
return lastProfileFetch;
}
public @Nullable String getNotificationChannel() {
return notificationChannel;
}

View File

@@ -366,11 +366,13 @@ public class SmsDatabase extends MessagingDatabase {
db.beginTransaction();
try {
String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)";
for (long id : ids) {
ContentValues contentValues = new ContentValues();
contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{String.valueOf(id)});
db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)});
if (threadId < 0) {
threadId = getThreadIdForMessage(id);

View File

@@ -90,6 +90,7 @@ public class ThreadDatabase extends Database {
public static final String EXPIRES_IN = "expires_in";
public static final String LAST_SEEN = "last_seen";
public static final String HAS_SENT = "has_sent";
private static final String LAST_SCROLLED = "last_scrolled";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " +
@@ -111,7 +112,8 @@ public class ThreadDatabase extends Database {
LAST_SEEN + " INTEGER DEFAULT 0, " +
HAS_SENT + " INTEGER DEFAULT 0, " +
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
UNREAD_COUNT + " INTEGER DEFAULT 0);";
UNREAD_COUNT + " INTEGER DEFAULT 0, " +
LAST_SCROLLED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
@@ -120,7 +122,7 @@ public class ThreadDatabase extends Database {
private static final String[] THREAD_PROJECTION = {
ID, DATE, MESSAGE_COUNT, RECIPIENT_ID, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE,
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, LAST_SCROLLED
};
private static final List<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
@@ -550,9 +552,21 @@ public class ThreadDatabase extends Database {
return positions;
}
public Cursor getConversationList(long offset, long limit) {
return getConversationList("0", offset, limit);
}
public Cursor getArchivedConversationList(long offset, long limit) {
return getConversationList("1", offset, limit);
}
private Cursor getConversationList(String archived) {
return getConversationList(archived, 0, 0);
}
private Cursor getConversationList(@NonNull String archived, long offset, long limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", 0);
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", offset, limit);
Cursor cursor = db.rawQuery(query, new String[]{archived});
setNotifyConversationListListeners(cursor);
@@ -560,20 +574,24 @@ public class ThreadDatabase extends Database {
return cursor;
}
public int getUnarchivedConversationListCount() {
return getConversationListCount(false);
}
public int getArchivedConversationListCount() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
return getConversationListCount(true);
}
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, ARCHIVED + " = ?",
new String[] {"1"}, null, null, null);
private int getConversationListCount(boolean archived) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] columns = new String[] { "COUNT(*)" };
String query = ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0";
String[] args = new String[] { archived ? "1" : "0" };
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0);
}
} finally {
if (cursor != null) cursor.close();
}
return 0;
@@ -618,18 +636,26 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
}
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
public void setLastScrolled(long threadId, long lastScrolledTimestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
try {
contentValues.put(LAST_SCROLLED, lastScrolledTimestamp);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
}
public ConversationMetadata getConversationMetadata(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT, LAST_SCROLLED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
return new Pair<>(cursor.getLong(0), cursor.getLong(1) == 1);
return new ConversationMetadata(cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN)),
cursor.getLong(cursor.getColumnIndexOrThrow(HAS_SENT)) == 1,
cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SCROLLED)));
}
return new Pair<>(-1L, false);
} finally {
if (cursor != null) cursor.close();
return new ConversationMetadata(-1L, false, -1);
}
}
@@ -844,7 +870,11 @@ public class ThreadDatabase extends Database {
return null;
}
private @NonNull String createQuery(@NonNull String where, int limit) {
private @NonNull String createQuery(@NonNull String where, long limit) {
return createQuery(where, 0, limit);
}
private @NonNull String createQuery(@NonNull String where, long offset, long limit) {
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
String query =
"SELECT " + projection + " FROM " + TABLE_NAME +
@@ -859,6 +889,10 @@ public class ThreadDatabase extends Database {
query += " LIMIT " + limit;
}
if (offset > 0) {
query += " OFFSET " + offset;
}
return query;
}
@@ -1060,4 +1094,28 @@ public class ThreadDatabase extends Database {
return value;
}
}
public static class ConversationMetadata {
private final long lastSeen;
private final boolean hasSent;
private final long lastScrolled;
public ConversationMetadata(long lastSeen, boolean hasSent, long lastScrolled) {
this.lastSeen = lastSeen;
this.hasSent = hasSent;
this.lastScrolled = lastScrolled;
}
public long getLastSeen() {
return lastSeen;
}
public boolean hasSent() {
return hasSent;
}
public long getLastScrolled() {
return lastScrolled;
}
}
}

View File

@@ -134,8 +134,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int SERVER_TIMESTAMP = 59;
private static final int REMOTE_DELETE = 60;
private static final int COLOR_MIGRATION = 61;
private static final int LAST_SCROLLED = 62;
private static final int LAST_PROFILE_FETCH = 63;
private static final int SERVER_DELIVERED_TIMESTAMP = 64;
private static final int DATABASE_VERSION = 61;
private static final int DATABASE_VERSION = 64;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -906,6 +909,18 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
}
if (oldVersion < LAST_SCROLLED) {
db.execSQL("ALTER TABLE thread ADD COLUMN last_scrolled INTEGER DEFAULT 0");
}
if (oldVersion < LAST_PROFILE_FETCH) {
db.execSQL("ALTER TABLE recipient ADD COLUMN last_profile_fetch INTEGER DEFAULT 0");
}
if (oldVersion < SERVER_DELIVERED_TIMESTAMP) {
db.execSQL("ALTER TABLE push ADD COLUMN server_delivered_timestamp INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -1,83 +0,0 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import java.util.LinkedList;
import java.util.List;
public class ConversationListLoader extends AbstractCursorLoader {
private final String filter;
private final boolean archived;
public ConversationListLoader(Context context, String filter, boolean archived) {
super(context);
this.filter = filter;
this.archived = archived;
}
@Override
public Cursor getCursor() {
if (filter != null && filter.trim().length() != 0) return getFilteredConversationList(filter);
else if (!archived) return getUnarchivedConversationList();
else return getArchivedConversationList();
}
private Cursor getUnarchivedConversationList() {
List<Cursor> cursorList = new LinkedList<>();
cursorList.add(DatabaseFactory.getThreadDatabase(context).getConversationList());
int archivedCount = DatabaseFactory.getThreadDatabase(context)
.getArchivedConversationListCount();
if (archivedCount > 0) {
MatrixCursor switchToArchiveCursor = new MatrixCursor(new String[] {
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
ThreadDatabase.RECIPIENT_ID, ThreadDatabase.SNIPPET, ThreadDatabase.READ, ThreadDatabase.UNREAD_COUNT,
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
ThreadDatabase.SNIPPET_CONTENT_TYPE, ThreadDatabase.SNIPPET_EXTRAS,
ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS, ThreadDatabase.DELIVERY_RECEIPT_COUNT,
ThreadDatabase.EXPIRES_IN, ThreadDatabase.LAST_SEEN, ThreadDatabase.READ_RECEIPT_COUNT}, 1);
if (cursorList.get(0).getCount() <= 0) {
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.INBOX_ZERO,
0, null, null, null, 0, -1, 0, 0, 0, -1});
}
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.ARCHIVE,
0, null, null, null, 0, -1, 0, 0, 0, -1});
cursorList.add(switchToArchiveCursor);
}
return new MergeCursor(cursorList.toArray(new Cursor[0]));
}
private Cursor getArchivedConversationList() {
return DatabaseFactory.getThreadDatabase(context).getArchivedConversationList();
}
private Cursor getFilteredConversationList(String filter) {
List<String> numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter);
List<RecipientId> recipientIds = new LinkedList<>();
for (String number : numbers) {
recipientIds.add(Recipient.external(context, number).getId());
}
return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(recipientIds);
}
}

View File

@@ -78,6 +78,10 @@ public abstract class DisplayRecord {
!MmsSmsColumns.Types.isIdentityDefault(type);
}
public boolean isSent() {
return MmsSmsColumns.Types.isSentType(type);
}
public boolean isOutgoing() {
return MmsSmsColumns.Types.isOutgoingMessageType(type);
}

View File

@@ -116,32 +116,32 @@ public abstract class MessageRecord extends DisplayRecord {
} else if (isGroupQuit() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
} else if (isGroupQuit()) {
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString(context)));
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().getDisplayName(context)));
} else if (isIncomingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().toShortString(context)));
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().getDisplayName(context)));
} else if (isOutgoingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
} else if (isMissedCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
} else if (isJoined()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().toShortString(context)));
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
} else if (isExpirationTimerUpdate()) {
int seconds = (int)(getExpiresIn() / 1000);
if (seconds <= 0) {
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().toShortString(context)));
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().getDisplayName(context)));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(context), time));
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().getDisplayName(context), time));
} else if (isIdentityUpdate()) {
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString(context)));
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().getDisplayName(context)));
} else if (isIdentityVerified()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString(context)));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString(context)));
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().getDisplayName(context)));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().getDisplayName(context)));
} else if (isIdentityDefault()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString(context)));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString(context)));
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().getDisplayName(context)));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().getDisplayName(context)));
}
return new SpannableString(getBody());
@@ -178,7 +178,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
/**
* Describes a UUID by it's corresponding recipient's {@link Recipient#toShortString}.
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
*/
private static class ShortStringDescriptionStrategy implements GroupsV2UpdateMessageProducer.DescribeMemberStrategy {
@@ -193,7 +193,7 @@ public abstract class MessageRecord extends DisplayRecord {
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
return context.getString(R.string.MessageRecord_unknown);
}
return Recipient.resolved(RecipientId.from(uuid, null)).toShortString(context);
return Recipient.resolved(RecipientId.from(uuid, null)).getDisplayName(context);
}
}

View File

@@ -84,7 +84,7 @@ public class SmsMessageRecord extends MessageRecord {
} else if (isEndSession() && isOutgoing()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
} else if (isEndSession()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().toShortString(context)));
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().getDisplayName(context)));
} else if (SmsDatabase.Types.isUnsupportedMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version));
} else if (SmsDatabase.Types.isInvalidMessageType(type)) {

View File

@@ -187,6 +187,53 @@ public final class ThreadRecord {
else return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThreadRecord that = (ThreadRecord) o;
return threadId == that.threadId &&
type == that.type &&
date == that.date &&
deliveryStatus == that.deliveryStatus &&
deliveryReceiptCount == that.deliveryReceiptCount &&
readReceiptCount == that.readReceiptCount &&
count == that.count &&
unreadCount == that.unreadCount &&
forcedUnread == that.forcedUnread &&
distributionType == that.distributionType &&
archived == that.archived &&
expiresIn == that.expiresIn &&
lastSeen == that.lastSeen &&
body.equals(that.body) &&
recipient.equals(that.recipient) &&
Objects.equals(snippetUri, that.snippetUri) &&
Objects.equals(contentType, that.contentType) &&
Objects.equals(extra, that.extra);
}
@Override
public int hashCode() {
return Objects.hash(threadId,
body,
recipient,
type,
date,
deliveryStatus,
deliveryReceiptCount,
readReceiptCount,
snippetUri,
contentType,
extra,
count,
unreadCount,
forcedUnread,
distributionType,
archived,
expiresIn,
lastSeen);
}
public static class Builder {
private long threadId;
private String body;

View File

@@ -46,6 +46,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
import java.util.UUID;
import java.util.concurrent.Executors;
/**
* Implementation of {@link ApplicationDependencies.Provider} that provides real app dependencies.
@@ -91,7 +92,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)),
provideClientZkOperations().getProfileOperations(),
SignalExecutors.UNBOUNDED);
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16));
}
@Override

View File

@@ -110,7 +110,7 @@ public final class GroupV1MessageProcessor {
Recipient sender = Recipient.externalPush(context, content.getSender());
if (FeatureFlags.messageRequests() && (sender.isSystemContact() || sender.isProfileSharing())) {
if (sender.isSystemContact() || sender.isProfileSharing()) {
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.externalGroup(context, id).getId(), true);
}
@@ -245,7 +245,7 @@ public final class GroupV1MessageProcessor {
} else {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, storage, body);
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
@@ -52,7 +53,13 @@ public final class LiveGroup {
}
public LiveData<String> getTitle() {
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getTitle);
return LiveDataUtil.combineLatest(groupRecord, recipient, (groupRecord, recipient) -> {
String title = groupRecord.getTitle();
if (!TextUtils.isEmpty(title)) {
return title;
}
return recipient.getDisplayName(ApplicationDependencies.getApplication());
});
}
public LiveData<Recipient> getGroupRecipient() {

View File

@@ -15,12 +15,10 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupViewModel.Event;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
@@ -44,10 +42,6 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
@NonNull RecipientId recipientId,
@NonNull List<RecipientId> currentGroupsMemberOf)
{
if (!FeatureFlags.newGroupUI()) {
return new Intent(context, GroupCreateActivity.class);
}
Intent intent = new Intent(context, AddToGroupsActivity.class);
intent.putExtra(ContactSelectionListFragment.MULTI_SELECT, false);

View File

@@ -13,7 +13,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
@@ -30,10 +29,6 @@ public class CreateGroupActivity extends ContactSelectionActivity {
private View next;
public static Intent newIntent(@NonNull Context context) {
if (!FeatureFlags.newGroupUI()) {
return new Intent(context, GroupCreateActivity.class);
}
Intent intent = new Intent(context, CreateGroupActivity.class);
intent.putExtra(ContactSelectionListFragment.MULTI_SELECT, true);

View File

@@ -8,10 +8,7 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
@@ -94,7 +91,7 @@ public class AddGroupDetailsFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
create = view.findViewById(R.id.create);
name = view.findViewById(R.id.group_name);
name = view.findViewById(R.id.name);
toolbar = view.findViewById(R.id.toolbar);
setCreateEnabled(false, false);
@@ -116,7 +113,13 @@ public class AddGroupDetailsFragment extends Fragment {
name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString())));
toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed());
create.setOnClickListener(v -> handleCreateClicked());
viewModel.getMembers().observe(getViewLifecycleOwner(), members::setMembers);
viewModel.getMembers().observe(getViewLifecycleOwner(), recipients -> {
members.setMembers(recipients);
if (recipients.isEmpty()) {
toast(R.string.AddGroupDetailsFragment__groups_require_at_least_two_members);
callback.onNavigationButtonPressed();
}
});
viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
@@ -226,6 +229,7 @@ public class AddGroupDetailsFragment extends Fragment {
break;
case ERROR_INVALID_MEMBER_COUNT:
toast(R.string.AddGroupDetailsFragment__groups_require_at_least_two_members);
callback.onNavigationButtonPressed();
break;
default:
throw new IllegalStateException("Unexpected error: " + error.getErrorType().name());

View File

@@ -22,7 +22,7 @@ public class ManageGroupActivity extends PassphraseRequiredActionBarActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context, @NonNull GroupId.Push groupId) {
public static Intent newIntent(@NonNull Context context, @NonNull GroupId groupId) {
Intent intent = new Intent(context, ManageGroupActivity.class);
intent.putExtra(GROUP_ID, groupId.toString());
return intent;
@@ -44,6 +44,7 @@ public class ManageGroupActivity extends PassphraseRequiredActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
getWindow().getDecorView().setSystemUiVisibility(getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
setContentView(R.layout.group_manage_activity);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()

View File

@@ -27,15 +27,15 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GroupFallbackPhoto80;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
import org.thoughtcrime.securesms.groups.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
@@ -79,8 +80,10 @@ public class ManageGroupFragment extends Fragment {
private TextView editGroupAccessValue;
private View editGroupMembershipRow;
private TextView editGroupMembershipValue;
private View disappearingMessagesCard;
private View disappearingMessagesRow;
private TextView disappearingMessages;
private View blockAndLeaveCard;
private TextView blockGroup;
private TextView unblockGroup;
private TextView leaveGroup;
@@ -95,7 +98,7 @@ public class ManageGroupFragment extends Fragment {
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
@Override
public @NonNull FallbackContactPhoto getPhotoForGroup() {
return new GroupFallbackPhoto80();
return new FallbackPhoto80dp(R.drawable.ic_group_80, MaterialColor.ULTRAMARINE);
}
};
@@ -118,7 +121,7 @@ public class ManageGroupFragment extends Fragment {
avatar = view.findViewById(R.id.group_avatar);
toolbar = view.findViewById(R.id.toolbar);
groupName = view.findViewById(R.id.group_name);
groupName = view.findViewById(R.id.name);
memberCountUnderAvatar = view.findViewById(R.id.member_count);
memberCountAboveList = view.findViewById(R.id.member_count_2);
groupMemberList = view.findViewById(R.id.group_members);
@@ -133,8 +136,10 @@ public class ManageGroupFragment extends Fragment {
editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
editGroupMembershipRow = view.findViewById(R.id.edit_group_membership_row);
editGroupMembershipValue = view.findViewById(R.id.edit_group_membership_value);
disappearingMessagesCard = view.findViewById(R.id.group_disappearing_messages_card);
disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
disappearingMessages = view.findViewById(R.id.disappearing_messages);
blockAndLeaveCard = view.findViewById(R.id.group_block_and_leave_card);
blockGroup = view.findViewById(R.id.blockGroup);
unblockGroup = view.findViewById(R.id.unblockGroup);
leaveGroup = view.findViewById(R.id.leaveGroup);
@@ -154,9 +159,12 @@ public class ManageGroupFragment extends Fragment {
super.onActivityCreated(savedInstanceState);
Context context = requireContext();
GroupId.Push groupId = getPushGroupId();
GroupId groupId = getGroupId();
ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
disappearingMessagesCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
blockAndLeaveCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
viewModel.getMembers().observe(getViewLifecycleOwner(), members -> groupMemberList.setMembers(members));
@@ -205,6 +213,8 @@ public class ManageGroupFragment extends Fragment {
activity.startActivity(AvatarPreviewActivity.intentFromRecipientId(activity, groupRecipient.getId()),
AvatarPreviewActivity.createTransitionBundle(activity, avatar));
});
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
.show(requireFragmentManager(), "CUSTOM_NOTIFICATIONS"));
});
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
@@ -299,9 +309,6 @@ public class ManageGroupFragment extends Fragment {
customNotificationsRow.setVisibility(View.VISIBLE);
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupId)
.show(requireFragmentManager(), "CUSTOM_NOTIFICATIONS"));
//noinspection CodeBlock2Expr
if (NotificationChannels.supported()) {
viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
@@ -321,15 +328,15 @@ public class ManageGroupFragment extends Fragment {
public boolean onMenuItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_edit) {
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getPushGroupId()));
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId().requirePush()));
return true;
}
return false;
}
private GroupId.Push getPushGroupId() {
return GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requirePush();
private GroupId getGroupId() {
return GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID)));
}
private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {

View File

@@ -7,10 +7,8 @@ import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
@@ -33,21 +31,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
final class ManageGroupRepository {
private static final String TAG = Log.tag(ManageGroupRepository.class);
private final Context context;
private final GroupId.Push groupId;
private final Context context;
private final GroupId groupId;
ManageGroupRepository(@NonNull Context context, @NonNull GroupId.Push groupId) {
ManageGroupRepository(@NonNull Context context, @NonNull GroupId groupId) {
this.context = context;
this.groupId = groupId;
}
@@ -149,7 +145,7 @@ final class ManageGroupRepository {
void addMembers(@NonNull List<RecipientId> selected, @NonNull AddMembersResultCallback addMembersResultCallback, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.addMembers(context, groupId, selected);
GroupManager.addMembers(context, groupId.requirePush(), selected);
addMembersResultCallback.onMembersAdded(selected.size());
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
Log.w(TAG, e);

View File

@@ -333,10 +333,10 @@ public class ManageGroupViewModel extends ViewModel {
}
public static class Factory implements ViewModelProvider.Factory {
private final Context context;
private final GroupId.Push groupId;
private final Context context;
private final GroupId groupId;
public Factory(@NonNull Context context, @NonNull GroupId.Push groupId) {
public Factory(@NonNull Context context, @NonNull GroupId groupId) {
this.context = context;
this.groupId = groupId;
}

View File

@@ -1,182 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.notifications;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.ThemeUtil;
public class CustomNotificationsDialogFragment extends DialogFragment {
private static final short RINGTONE_PICKER_REQUEST_CODE = 13562;
private static final String ARG_GROUP_ID = "group_id";
private SwitchCompat customNotificationsSwitch;
private View soundLabel;
private TextView soundSelector;
private View vibrateLabel;
private SwitchCompat vibrateSwitch;
private CustomNotificationsViewModel viewModel;
public static DialogFragment create(@NonNull GroupId groupId) {
DialogFragment fragment = new CustomNotificationsDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_GROUP_ID, groupId.toString());
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (ThemeUtil.isDarkTheme(requireActivity())) {
setStyle(STYLE_NO_FRAME, R.style.TextSecure_DarkTheme);
} else {
setStyle(STYLE_NO_FRAME, R.style.TextSecure_LightTheme);
}
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.custom_notifications_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeViewModel();
initializeViews(view);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == RINGTONE_PICKER_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
viewModel.setMessageSound(uri);
}
}
private void initializeViewModel() {
Bundle arguments = requireArguments();
GroupId groupId = GroupId.parseOrThrow(arguments.getString(ARG_GROUP_ID, ""));
CustomNotificationsRepository repository = new CustomNotificationsRepository(requireContext(), groupId);
CustomNotificationsViewModel.Factory factory = new CustomNotificationsViewModel.Factory(groupId, repository);
viewModel = ViewModelProviders.of(this, factory).get(CustomNotificationsViewModel.class);
}
private void initializeViews(@NonNull View view) {
customNotificationsSwitch = view.findViewById(R.id.custom_notifications_enable_switch);
soundLabel = view.findViewById(R.id.custom_notifications_sound_label);
soundSelector = view.findViewById(R.id.custom_notifications_sound_selection);
vibrateLabel = view.findViewById(R.id.custom_notifications_vibrate_label);
vibrateSwitch = view.findViewById(R.id.custom_notifications_vibrate_switch);
Toolbar toolbar = view.findViewById(R.id.custom_notifications_toolbar);
toolbar.setNavigationOnClickListener(v -> dismissAllowingStateLoss());
CompoundButton.OnCheckedChangeListener onCustomNotificationsSwitchCheckChangedListener = (buttonView, isChecked) -> {
viewModel.setHasCustomNotifications(isChecked);
};
viewModel.isInitialLoadComplete().observe(getViewLifecycleOwner(), customNotificationsSwitch::setEnabled);
viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
if (customNotificationsSwitch.isChecked() != hasCustomNotifications) {
customNotificationsSwitch.setOnCheckedChangeListener(null);
customNotificationsSwitch.setChecked(hasCustomNotifications);
}
customNotificationsSwitch.setOnCheckedChangeListener(onCustomNotificationsSwitchCheckChangedListener);
soundLabel.setEnabled(hasCustomNotifications);
vibrateLabel.setEnabled(hasCustomNotifications);
soundSelector.setVisibility(hasCustomNotifications ? View.VISIBLE : View.GONE);
vibrateSwitch.setVisibility(hasCustomNotifications ? View.VISIBLE : View.GONE);
});
if (!NotificationChannels.supported()) {
customNotificationsSwitch.setVisibility(View.GONE);
view.findViewById(R.id.custom_notifications_enable_label).setVisibility(View.GONE);
}
CompoundButton.OnCheckedChangeListener onVibrateSwitchCheckChangedListener = (buttonView, isChecked) -> {
viewModel.setMessageVibrate(isChecked ? RecipientDatabase.VibrateState.ENABLED : RecipientDatabase.VibrateState.DISABLED);
};
viewModel.getVibrateState().observe(getViewLifecycleOwner(), vibrateState -> {
boolean vibrateEnabled = vibrateState != RecipientDatabase.VibrateState.DISABLED;
if (vibrateSwitch.isChecked() != vibrateEnabled) {
vibrateSwitch.setOnCheckedChangeListener(null);
vibrateSwitch.setChecked(vibrateEnabled);
}
vibrateSwitch.setOnCheckedChangeListener(onVibrateSwitchCheckChangedListener);
});
viewModel.getNotificationSound().observe(getViewLifecycleOwner(), sound -> {
soundSelector.setText(getRingtoneSummary(requireContext(), sound));
soundSelector.setTag(sound);
});
soundSelector.setOnClickListener(v -> launchSoundSelector(viewModel.getNotificationSound().getValue()));
}
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
if (ringtone == null) {
return context.getString(R.string.preferences__default);
} else if (ringtone.toString().isEmpty()) {
return context.getString(R.string.preferences__silent);
} else {
Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone);
if (tone != null) {
return tone.getTitle(context);
}
}
return context.getString(R.string.preferences__default);
}
private void launchSoundSelector(@Nullable Uri current) {
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
startActivityForResult(intent, RINGTONE_PICKER_REQUEST_CODE);
}
}

View File

@@ -1,82 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.notifications;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
public final class CustomNotificationsViewModel extends ViewModel {
private final LiveGroup liveGroup;
private final LiveData<Boolean> hasCustomNotifications;
private final LiveData<RecipientDatabase.VibrateState> isVibrateEnabled;
private final LiveData<Uri> notificationSound;
private final CustomNotificationsRepository repository;
private final MutableLiveData<Boolean> isInitialLoadComplete = new MutableLiveData<>();
private CustomNotificationsViewModel(@NonNull GroupId groupId, @NonNull CustomNotificationsRepository repository) {
this.liveGroup = new LiveGroup(groupId);
this.repository = repository;
this.hasCustomNotifications = Transformations.map(liveGroup.getGroupRecipient(), recipient -> recipient.getNotificationChannel() != null || !NotificationChannels.supported());
this.isVibrateEnabled = Transformations.map(liveGroup.getGroupRecipient(), Recipient::getMessageVibrate);
this.notificationSound = Transformations.map(liveGroup.getGroupRecipient(), Recipient::getMessageRingtone);
repository.onLoad(() -> isInitialLoadComplete.postValue(true));
}
public LiveData<Boolean> isInitialLoadComplete() {
return isInitialLoadComplete;
}
public LiveData<Boolean> hasCustomNotifications() {
return hasCustomNotifications;
}
public LiveData<RecipientDatabase.VibrateState> getVibrateState() {
return isVibrateEnabled;
}
public LiveData<Uri> getNotificationSound() {
return notificationSound;
}
public void setHasCustomNotifications(boolean hasCustomNotifications) {
repository.setHasCustomNotifications(hasCustomNotifications);
}
public void setMessageVibrate(@NonNull RecipientDatabase.VibrateState vibrateState) {
repository.setMessageVibrate(vibrateState);
}
public void setMessageSound(@Nullable Uri sound) {
repository.setMessageSound(sound);
}
public static final class Factory implements ViewModelProvider.Factory {
private final GroupId groupId;
private final CustomNotificationsRepository repository;
public Factory(@NonNull GroupId groupId, @NonNull CustomNotificationsRepository repository) {
this.groupId = groupId;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new CustomNotificationsViewModel(groupId, repository));
}
}
}

View File

@@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
@@ -150,6 +151,14 @@ class JobController {
}
}
@WorkerThread
synchronized void cancelAllInQueue(@NonNull String queue) {
Stream.of(runningJobs.values())
.filter(j -> Objects.equals(j.getParameters().getQueue(), queue))
.map(Job::getId)
.forEach(this::cancelJob);
}
@WorkerThread
synchronized void onRetry(@NonNull Job job) {
int nextRunAttempt = job.getRunAttempt() + 1;

View File

@@ -198,6 +198,13 @@ public class JobManager implements ConstraintObserver.Notifier {
executor.execute(() -> jobController.cancelJob(id));
}
/**
* Cancels all jobs in the specified queue. See {@link #cancel(String)} for details.
*/
public void cancelAllInQueue(@NonNull String queue) {
executor.execute(() -> jobController.cancelAllInQueue(queue));
}
/**
* Runs the specified job synchronously. Beware: All normal dependencies are respected, meaning
* you must take great care where you call this. It could take a very long time to complete!

View File

@@ -151,6 +151,9 @@ public class PushGroupSendJob extends PushSendJob {
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipient());
ApplicationDependencies.getJobManager().cancelAllInQueue(TypingSendJob.getQueue(threadId));
if (database.isSent(messageId)) {
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
return;

View File

@@ -501,13 +501,14 @@ public final class PushProcessMessageJob extends BaseJob {
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp())
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode())
.putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
.putExtra(WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP, content.getServerReceivedTimestamp())
.putExtra(WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP, content.getServerDeliveredTimestamp())
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode())
.putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent);
else context.startService(intent);
@@ -601,7 +602,7 @@ public final class PushProcessMessageJob extends BaseJob {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
content.getSenderDevice(),
content.getTimestamp(),
content.getServerTimestamp(),
content.getServerReceivedTimestamp(),
"", Optional.absent(), 0,
content.isNeedsReceipt());
@@ -710,7 +711,7 @@ public final class PushProcessMessageJob extends BaseJob {
Recipient sender = Recipient.externalPush(context, content.getSender());
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
content.getTimestamp(),
content.getServerTimestamp(),
content.getServerReceivedTimestamp(),
-1,
expiresInSeconds * 1000L,
true,
@@ -768,7 +769,7 @@ public final class PushProcessMessageJob extends BaseJob {
Recipient sender = Recipient.externalPush(context, content.getSender());
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(delete.getTargetSentTimestamp(), sender.getId());
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerTimestamp())) {
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerReceivedTimestamp())) {
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
db.markAsRemoteDelete(targetMessage.getId());
ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false);
@@ -777,7 +778,7 @@ public final class PushProcessMessageJob extends BaseJob {
ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), delete.getTargetSentTimestamp(), content);
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "[handleRemoteDelete] Invalid remote delete! deleteTime: %d, targetTime: %d, deleteAuthor: %s, targetAuthor: %s",
content.getServerTimestamp(), targetMessage.getServerTimestamp(), sender.getId(), targetMessage.getRecipient().getId()));
content.getServerReceivedTimestamp(), targetMessage.getServerTimestamp(), sender.getId(), targetMessage.getRecipient().getId()));
}
}
@@ -1025,7 +1026,7 @@ public final class PushProcessMessageJob extends BaseJob {
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Recipient.externalPush(context, content.getSender()).getId(),
message.getTimestamp(),
content.getServerTimestamp(),
content.getServerReceivedTimestamp(),
-1,
message.getExpiresInSeconds() * 1000L,
false,
@@ -1245,7 +1246,7 @@ public final class PushProcessMessageJob extends BaseJob {
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
content.getSenderDevice(),
message.getTimestamp(),
content.getServerTimestamp(),
content.getServerReceivedTimestamp(),
body,
groupId,
message.getExpiresInSeconds() * 1000L,
@@ -1772,8 +1773,15 @@ public final class PushProcessMessageJob extends BaseJob {
} else {
return sender.isBlocked();
}
} else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) {
} else if (content.getCallMessage().isPresent()) {
return sender.isBlocked();
} else if (content.getTypingMessage().isPresent()) {
if (content.getTypingMessage().get().getGroupId().isPresent()) {
GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get());
return Recipient.externalGroup(context, groupId).isBlocked();
} else {
return sender.isBlocked();
}
}
return false;

View File

@@ -45,7 +45,7 @@ public class RefreshPreKeysJob extends BaseJob {
}
public static void scheduleIfNecessary() {
long timeSinceLastRefresh = System.currentTimeMillis() - SignalStore.getLastPrekeyRefreshTime();
long timeSinceLastRefresh = System.currentTimeMillis() - SignalStore.misc().getLastPrekeyRefreshTime();
if (timeSinceLastRefresh > REFRESH_INTERVAL) {
Log.i(TAG, "Scheduling a prekey refresh. Time since last schedule: " + timeSinceLastRefresh + " ms");
@@ -82,7 +82,7 @@ public class RefreshPreKeysJob extends BaseJob {
if (availableKeys >= PREKEY_MINIMUM && TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.i(TAG, "Available keys sufficient.");
SignalStore.setLastPrekeyRefreshTime(System.currentTimeMillis());
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
return;
}
@@ -98,7 +98,7 @@ public class RefreshPreKeysJob extends BaseJob {
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
ApplicationDependencies.getJobManager().add(new CleanPreKeysJob());
SignalStore.setLastPrekeyRefreshTime(System.currentTimeMillis());
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
Log.i(TAG, "Successfully refreshed prekeys.");
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Application;
import android.content.Context;
import android.text.TextUtils;
@@ -22,7 +23,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -32,6 +33,7 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@@ -48,7 +50,6 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -128,6 +129,36 @@ public class RetrieveProfileJob extends BaseJob {
}
}
/**
* Will fetch some profiles to ensure we're decently up-to-date if we haven't done so within a
* certain time period.
*/
public static void enqueueRoutineFetchIfNeccessary(Application application) {
long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastProfileRefreshTime();
if (timeSinceRefresh < TimeUnit.HOURS.toMillis(12)) {
Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago.");
return;
}
SignalExecutors.BOUNDED.execute(() -> {
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(application);
long current = System.currentTimeMillis();
List<RecipientId> ids = db.getRecipientsForRoutineProfileFetch(current - TimeUnit.DAYS.toMillis(30),
current - TimeUnit.DAYS.toMillis(1),
50);
if (ids.size() > 0) {
Log.i(TAG, "Optimistically refreshing " + ids.size() + " eligible recipient(s).");
enqueue(ids);
} else {
Log.i(TAG, "No recipients to refresh.");
}
SignalStore.misc().setLastProfileRefreshTime(System.currentTimeMillis());
});
}
private RetrieveProfileJob(@NonNull List<RecipientId> recipientIds) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
@@ -197,6 +228,9 @@ public class RetrieveProfileJob extends BaseJob {
process(profile.first(), profile.second());
}
Set<RecipientId> success = SetUtil.difference(recipientIds, retries);
DatabaseFactory.getRecipientDatabase(context).markProfilesFetched(success, System.currentTimeMillis());
stopwatch.split("process");
long keyCount = Stream.of(profiles).map(Pair::first).map(Recipient::getProfileKey).withoutNulls().count();

View File

@@ -97,7 +97,12 @@ public class SendReadReceiptJob extends BaseJob {
return;
}
Recipient recipient = Recipient.resolved(recipientId);
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isBlocked()) {
Log.w(TAG, "Refusing to send receipts to blocked recipient");
return;
}
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp);

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.CancelationException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
@@ -39,7 +40,7 @@ public class TypingSendJob extends BaseJob {
public TypingSendJob(long threadId, boolean typing) {
this(new Job.Parameters.Builder()
.setQueue("TYPING_" + threadId)
.setQueue(getQueue(threadId))
.setMaxAttempts(1)
.setLifespan(TimeUnit.SECONDS.toMillis(5))
.build(),
@@ -47,6 +48,10 @@ public class TypingSendJob extends BaseJob {
typing);
}
public static String getQueue(long threadId) {
return "TYPING_" + threadId;
}
private TypingSendJob(@NonNull Job.Parameters parameters, long threadId, boolean typing) {
super(parameters);
@@ -101,7 +106,16 @@ public class TypingSendJob extends BaseJob {
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList();
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage);
if (isCanceled()) {
Log.w(TAG, "Canceled before send!");
return;
}
try {
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled);
} catch (CancelationException e) {
Log.w(TAG, "Canceled during send!");
}
}
@Override

View File

@@ -12,7 +12,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
import java.io.IOException;
import java.security.SecureRandom;
public final class KbsValues {
public final class KbsValues extends SignalStoreValues {
public static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled";
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
@@ -21,10 +21,12 @@ public final class KbsValues {
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp";
private final KeyValueStore store;
KbsValues(KeyValueStore store) {
this.store = store;
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
/**
@@ -33,13 +35,13 @@ public final class KbsValues {
* Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}
*/
public void clearRegistrationLockAndPin() {
store.beginWrite()
.remove(V2_LOCK_ENABLED)
.remove(TOKEN_RESPONSE)
.remove(LOCK_LOCAL_PIN_HASH)
.remove(PIN)
.remove(LAST_CREATE_FAILED_TIMESTAMP)
.commit();
getStore().beginWrite()
.remove(V2_LOCK_ENABLED)
.remove(TOKEN_RESPONSE)
.remove(LOCK_LOCAL_PIN_HASH)
.remove(PIN)
.remove(LAST_CREATE_FAILED_TIMESTAMP)
.commit();
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
@@ -52,43 +54,43 @@ public final class KbsValues {
throw new AssertionError(e);
}
store.beginWrite()
.putString(TOKEN_RESPONSE, tokenResponse)
.putBlob(MASTER_KEY, masterKey.serialize())
.putString(LOCK_LOCAL_PIN_HASH, PinHashing.localPinHash(pin))
.putString(PIN, pin)
.putLong(LAST_CREATE_FAILED_TIMESTAMP, -1)
.commit();
getStore().beginWrite()
.putString(TOKEN_RESPONSE, tokenResponse)
.putBlob(MASTER_KEY, masterKey.serialize())
.putString(LOCK_LOCAL_PIN_HASH, PinHashing.localPinHash(pin))
.putString(PIN, pin)
.putLong(LAST_CREATE_FAILED_TIMESTAMP, -1)
.commit();
}
synchronized void setPinIfNotPresent(@NonNull String pin) {
if (store.getString(PIN, null) == null) {
store.beginWrite().putString(PIN, pin).commit();
if (getStore().getString(PIN, null) == null) {
getStore().beginWrite().putString(PIN, pin).commit();
}
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
public synchronized void setV2RegistrationLockEnabled(boolean enabled) {
store.beginWrite().putBoolean(V2_LOCK_ENABLED, enabled).apply();
putBoolean(V2_LOCK_ENABLED, enabled);
}
/**
* Whether or not registration lock V2 is enabled.
*/
public synchronized boolean isV2RegistrationLockEnabled() {
return store.getBoolean(V2_LOCK_ENABLED, false);
return getBoolean(V2_LOCK_ENABLED, false);
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
public synchronized void onPinCreateFailure() {
store.beginWrite().putLong(LAST_CREATE_FAILED_TIMESTAMP, System.currentTimeMillis()).apply();
putLong(LAST_CREATE_FAILED_TIMESTAMP, System.currentTimeMillis());
}
/**
* Whether or not the last time the user attempted to create a PIN, it failed.
*/
public synchronized boolean lastPinCreateFailed() {
return store.getLong(LAST_CREATE_FAILED_TIMESTAMP, -1) > 0;
return getLong(LAST_CREATE_FAILED_TIMESTAMP, -1) > 0;
}
/**
@@ -98,13 +100,13 @@ public final class KbsValues {
* If you only want a key when it's backed up, use {@link #getPinBackedMasterKey()}.
*/
public synchronized @NonNull MasterKey getOrCreateMasterKey() {
byte[] blob = store.getBlob(MASTER_KEY, null);
byte[] blob = getStore().getBlob(MASTER_KEY, null);
if (blob == null) {
store.beginWrite()
.putBlob(MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize())
.commit();
blob = store.getBlob(MASTER_KEY, null);
getStore().beginWrite()
.putBlob(MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize())
.commit();
blob = getBlob(MASTER_KEY, null);
}
return new MasterKey(blob);
@@ -119,7 +121,7 @@ public final class KbsValues {
}
private synchronized @Nullable MasterKey getMasterKey() {
byte[] blob = store.getBlob(MASTER_KEY, null);
byte[] blob = getBlob(MASTER_KEY, null);
return blob != null ? new MasterKey(blob) : null;
}
@@ -133,7 +135,7 @@ public final class KbsValues {
}
public synchronized @Nullable String getLocalPinHash() {
return store.getString(LOCK_LOCAL_PIN_HASH, null);
return getString(LOCK_LOCAL_PIN_HASH, null);
}
public synchronized boolean hasPin() {
@@ -141,7 +143,7 @@ public final class KbsValues {
}
public synchronized @Nullable TokenResponse getRegistrationLockTokenResponse() {
String token = store.getString(TOKEN_RESPONSE, null);
String token = getStore().getString(TOKEN_RESPONSE, null);
if (token == null) return null;

View File

@@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
public final class MiscellaneousValues extends SignalStoreValues {
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
putLong(MESSAGE_REQUEST_ENABLE_TIME, System.currentTimeMillis());
}
public long getLastPrekeyRefreshTime() {
return getLong(LAST_PREKEY_REFRESH_TIME, 0);
}
public void setLastPrekeyRefreshTime(long time) {
putLong(LAST_PREKEY_REFRESH_TIME, time);
}
public long getMessageRequestEnableTime() {
return getLong(MESSAGE_REQUEST_ENABLE_TIME, System.currentTimeMillis());
}
public long getLastProfileRefreshTime() {
return getLong(LAST_PROFILE_REFRESH_TIME, 0);
}
public void setLastProfileRefreshTime(long time) {
putLong(LAST_PROFILE_REFRESH_TIME, time);
}
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Specifically handles just the UI/UX state around PINs. For actual keys, see {@link KbsValues}.
*/
public final class PinValues {
public final class PinValues extends SignalStoreValues {
private static final String TAG = Log.tag(PinValues.class);
@@ -22,20 +22,22 @@ public final class PinValues {
private static final String PIN_STATE = "pin.pin_state";
public static final String PIN_REMINDERS_ENABLED = "pin.pin_reminders_enabled";
private final KeyValueStore store;
PinValues(KeyValueStore store) {
this.store = store;
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public void onEntrySuccess(@NonNull String pin) {
long nextInterval = SignalPinReminders.getNextInterval(getCurrentInterval());
Log.i(TAG, "onEntrySuccess() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
getStore().beginWrite()
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
SignalStore.kbsValues().setPinIfNotPresent(pin);
}
@@ -44,10 +46,10 @@ public final class PinValues {
long nextInterval = SignalPinReminders.getPreviousInterval(getCurrentInterval());
Log.i(TAG, "onEntrySuccessWithWrongGuess() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
getStore().beginWrite()
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
SignalStore.kbsValues().setPinIfNotPresent(pin);
}
@@ -56,61 +58,55 @@ public final class PinValues {
long nextInterval = SignalPinReminders.getPreviousInterval(getCurrentInterval());
Log.i(TAG, "onEntrySkipWithWrongGuess() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
putLong(NEXT_INTERVAL, nextInterval);
}
public void resetPinReminders() {
long nextInterval = SignalPinReminders.INITIAL_INTERVAL;
Log.i(TAG, "resetPinReminders() nextInterval: " + nextInterval, new Throwable());
store.beginWrite()
.putLong(NEXT_INTERVAL, nextInterval)
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.apply();
getStore().beginWrite()
.putLong(NEXT_INTERVAL, nextInterval)
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.apply();
}
public long getCurrentInterval() {
return store.getLong(NEXT_INTERVAL, TextSecurePreferences.getRegistrationLockNextReminderInterval(ApplicationDependencies.getApplication()));
return getLong(NEXT_INTERVAL, TextSecurePreferences.getRegistrationLockNextReminderInterval(ApplicationDependencies.getApplication()));
}
public long getLastSuccessfulEntryTime() {
return store.getLong(LAST_SUCCESSFUL_ENTRY, TextSecurePreferences.getRegistrationLockLastReminderTime(ApplicationDependencies.getApplication()));
return getLong(LAST_SUCCESSFUL_ENTRY, TextSecurePreferences.getRegistrationLockLastReminderTime(ApplicationDependencies.getApplication()));
}
public void setKeyboardType(@NonNull PinKeyboardType keyboardType) {
store.beginWrite()
.putString(KEYBOARD_TYPE, keyboardType.getCode())
.commit();
putString(KEYBOARD_TYPE, keyboardType.getCode());
}
public void setPinRemindersEnabled(boolean enabled) {
store.beginWrite().putBoolean(PIN_REMINDERS_ENABLED, enabled).apply();
putBoolean(PIN_REMINDERS_ENABLED, enabled);
}
public boolean arePinRemindersEnabled() {
return store.getBoolean(PIN_REMINDERS_ENABLED, true);
return getBoolean(PIN_REMINDERS_ENABLED, true);
}
public @NonNull PinKeyboardType getKeyboardType() {
return PinKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null));
return PinKeyboardType.fromCode(getStore().getString(KEYBOARD_TYPE, null));
}
public void setNextReminderIntervalToAtMost(long maxInterval) {
if (store.getLong(NEXT_INTERVAL, 0) > maxInterval) {
store.beginWrite()
.putLong(NEXT_INTERVAL, maxInterval)
.apply();
if (getStore().getLong(NEXT_INTERVAL, 0) > maxInterval) {
putLong(NEXT_INTERVAL, maxInterval);
}
}
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState} */
public void setPinState(@NonNull String pinState) {
store.beginWrite().putString(PIN_STATE, pinState).commit();
getStore().beginWrite().putString(PIN_STATE, pinState).commit();
}
public @Nullable String getPinState() {
return store.getString(PIN_STATE, null);
return getString(PIN_STATE, null);
}
}

View File

@@ -3,22 +3,20 @@ package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
public final class RegistrationValues {
public final class RegistrationValues extends SignalStoreValues {
private static final String REGISTRATION_COMPLETE = "registration.complete";
private static final String PIN_REQUIRED = "registration.pin_required";
private final KeyValueStore store;
RegistrationValues(@NonNull KeyValueStore store) {
this.store = store;
super(store);
}
public synchronized void onFirstEverAppLaunch() {
store.beginWrite()
.putBoolean(REGISTRATION_COMPLETE, false)
.putBoolean(PIN_REQUIRED, true)
.commit();
getStore().beginWrite()
.putBoolean(REGISTRATION_COMPLETE, false)
.putBoolean(PIN_REQUIRED, true)
.commit();
}
public synchronized void clearRegistrationComplete() {
@@ -26,18 +24,18 @@ public final class RegistrationValues {
}
public synchronized void setRegistrationComplete() {
store.beginWrite()
.putBoolean(REGISTRATION_COMPLETE, true)
.commit();
getStore().beginWrite()
.putBoolean(REGISTRATION_COMPLETE, true)
.commit();
}
@CheckResult
public synchronized boolean pinWasRequiredAtRegistration() {
return store.getBoolean(PIN_REQUIRED, false);
return getStore().getBoolean(PIN_REQUIRED, false);
}
@CheckResult
public synchronized boolean isRegistrationComplete() {
return store.getBoolean(REGISTRATION_COMPLETE, true);
return getStore().getBoolean(REGISTRATION_COMPLETE, true);
}
}

View File

@@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
public final class RemoteConfigValues {
public final class RemoteConfigValues extends SignalStoreValues {
private static final String TAG = Log.tag(RemoteConfigValues.class);
@@ -10,33 +12,35 @@ public final class RemoteConfigValues {
private static final String PENDING_CONFIG = "pending_remote_config";
private static final String LAST_FETCH_TIME = "remote_config_last_fetch_time";
private final KeyValueStore store;
RemoteConfigValues(@NonNull KeyValueStore store) {
super(store);
}
RemoteConfigValues(KeyValueStore store) {
this.store = store;
@Override
void onFirstEverAppLaunch() {
}
public String getCurrentConfig() {
return store.getString(CURRENT_CONFIG, null);
return getString(CURRENT_CONFIG, null);
}
public void setCurrentConfig(String value) {
store.beginWrite().putString(CURRENT_CONFIG, value).apply();
putString(CURRENT_CONFIG, value);
}
public String getPendingConfig() {
return store.getString(PENDING_CONFIG, getCurrentConfig());
return getString(PENDING_CONFIG, getCurrentConfig());
}
public void setPendingConfig(String value) {
store.beginWrite().putString(PENDING_CONFIG, value).apply();
putString(PENDING_CONFIG, value);
}
public long getLastFetchTime() {
return store.getLong(LAST_FETCH_TIME, 0);
return getLong(LAST_FETCH_TIME, 0);
}
public void setLastFetchTime(long time) {
store.beginWrite().putLong(LAST_FETCH_TIME, time).apply();
putLong(LAST_FETCH_TIME, time);
}
}

View File

@@ -12,65 +12,77 @@ import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
*/
public final class SignalStore {
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
private static final SignalStore INSTANCE = new SignalStore();
private SignalStore() {}
private final KeyValueStore store;
private final KbsValues kbsValues;
private final RegistrationValues registrationValues;
private final PinValues pinValues;
private final RemoteConfigValues remoteConfigValues;
private final StorageServiceValues storageServiceValues;
private final UiHints uiHints;
private final TooltipValues tooltipValues;
private final MiscellaneousValues misc;
private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore();
this.kbsValues = new KbsValues(store);
this.registrationValues = new RegistrationValues(store);
this.pinValues = new PinValues(store);
this.remoteConfigValues = new RemoteConfigValues(store);
this.storageServiceValues = new StorageServiceValues(store);
this.uiHints = new UiHints(store);
this.tooltipValues = new TooltipValues(store);
this.misc = new MiscellaneousValues(store);
}
public static void onFirstEverAppLaunch() {
kbsValues().onFirstEverAppLaunch();
registrationValues().onFirstEverAppLaunch();
pinValues().onFirstEverAppLaunch();
remoteConfigValues().onFirstEverAppLaunch();
storageServiceValues().onFirstEverAppLaunch();
uiHints().onFirstEverAppLaunch();
tooltips().onFirstEverAppLaunch();
misc().onFirstEverAppLaunch();
}
public static @NonNull KbsValues kbsValues() {
return new KbsValues(getStore());
return INSTANCE.kbsValues;
}
public static @NonNull RegistrationValues registrationValues() {
return new RegistrationValues(getStore());
return INSTANCE.registrationValues;
}
public static @NonNull PinValues pinValues() {
return new PinValues(getStore());
return INSTANCE.pinValues;
}
public static @NonNull RemoteConfigValues remoteConfigValues() {
return new RemoteConfigValues(getStore());
return INSTANCE.remoteConfigValues;
}
public static @NonNull StorageServiceValues storageServiceValues() {
return new StorageServiceValues(getStore());
return INSTANCE.storageServiceValues;
}
public static @NonNull UiHints uiHints() {
return new UiHints(getStore());
return INSTANCE.uiHints;
}
public static @NonNull TooltipValues tooltips() {
return new TooltipValues(getStore());
return INSTANCE.tooltipValues;
}
public static @NonNull MiscellaneousValues misc() {
return INSTANCE.misc;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
}
public static long getLastPrekeyRefreshTime() {
return getStore().getLong(LAST_PREKEY_REFRESH_TIME, 0);
}
public static void setLastPrekeyRefreshTime(long time) {
putLong(LAST_PREKEY_REFRESH_TIME, time);
}
public static long getMessageRequestEnableTime() {
return getStore().getLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
}
public static void setMessageRequestEnableTime(long time) {
putLong(MESSAGE_REQUEST_ENABLE_TIME, time);
}
public static @NonNull PreferenceDataStore getPreferenceDataStore() {
return new SignalPreferenceDataStore(getStore());
}
@@ -84,30 +96,6 @@ public final class SignalStore {
}
private static @NonNull KeyValueStore getStore() {
return ApplicationDependencies.getKeyValueStore();
}
private static void putBlob(@NonNull String key, byte[] value) {
getStore().beginWrite().putBlob(key, value).apply();
}
private static void putBoolean(@NonNull String key, boolean value) {
getStore().beginWrite().putBoolean(key, value).apply();
}
private static void putFloat(@NonNull String key, float value) {
getStore().beginWrite().putFloat(key, value).apply();
}
private static void putInteger(@NonNull String key, int value) {
getStore().beginWrite().putInteger(key, value).apply();
}
private static void putLong(@NonNull String key, long value) {
getStore().beginWrite().putLong(key, value).apply();
}
private static void putString(@NonNull String key, String value) {
getStore().beginWrite().putString(key, value).apply();
return INSTANCE.store;
}
}

View File

@@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
abstract class SignalStoreValues {
private final KeyValueStore store;
SignalStoreValues(@NonNull KeyValueStore store) {
this.store = store;
}
@NonNull KeyValueStore getStore() {
return store;
}
abstract void onFirstEverAppLaunch();
String getString(String key, String defaultValue) {
return store.getString(key, defaultValue);
}
int getInteger(String key, int defaultValue) {
return store.getInteger(key, defaultValue);
}
long getLong(String key, long defaultValue) {
return store.getLong(key, defaultValue);
}
boolean getBoolean(String key, boolean defaultValue) {
return store.getBoolean(key, defaultValue);
}
float getFloat(String key, float defaultValue) {
return store.getFloat(key, defaultValue);
}
byte[] getBlob(String key, byte[] defaultValue) {
return store.getBlob(key, defaultValue);
}
void putBlob(@NonNull String key, byte[] value) {
store.beginWrite().putBlob(key, value).apply();
}
void putBoolean(@NonNull String key, boolean value) {
store.beginWrite().putBoolean(key, value).apply();
}
void putFloat(@NonNull String key, float value) {
store.beginWrite().putFloat(key, value).apply();
}
void putInteger(@NonNull String key, int value) {
store.beginWrite().putInteger(key, value).apply();
}
void putLong(@NonNull String key, long value) {
store.beginWrite().putLong(key, value).apply();
}
void putString(@NonNull String key, String value) {
store.beginWrite().putString(key, value).apply();
}
}

View File

@@ -8,15 +8,17 @@ import org.whispersystems.signalservice.api.storage.StorageKey;
import java.security.SecureRandom;
public class StorageServiceValues {
public class StorageServiceValues extends SignalStoreValues {
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore";
private final KeyValueStore store;
StorageServiceValues(@NonNull KeyValueStore store) {
this.store = store;
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public synchronized StorageKey getOrCreateStorageKey() {
@@ -24,18 +26,18 @@ public class StorageServiceValues {
}
public long getLastSyncTime() {
return store.getLong(LAST_SYNC_TIME, 0);
return getLong(LAST_SYNC_TIME, 0);
}
public void onSyncCompleted() {
store.beginWrite().putLong(LAST_SYNC_TIME, System.currentTimeMillis()).apply();
putLong(LAST_SYNC_TIME, System.currentTimeMillis());
}
public boolean needsAccountRestore() {
return store.getBoolean(NEEDS_ACCOUNT_RESTORE, false);
return getBoolean(NEEDS_ACCOUNT_RESTORE, false);
}
public void setNeedsAccountRestore(boolean value) {
store.beginWrite().putBoolean(NEEDS_ACCOUNT_RESTORE, value).apply();
putBoolean(NEEDS_ACCOUNT_RESTORE, value);
}
}

View File

@@ -4,24 +4,23 @@ import androidx.annotation.NonNull;
import org.whispersystems.signalservice.api.storage.StorageKey;
public class TooltipValues {
public class TooltipValues extends SignalStoreValues {
private static final String BLUR_HUD_ICON = "tooltip.blur_hud_icon";
private final KeyValueStore store;
TooltipValues(@NonNull KeyValueStore store) {
this.store = store;
super(store);
}
@Override
public void onFirstEverAppLaunch() {
}
public boolean hasSeenBlurHudIconTooltip() {
return store.getBoolean(BLUR_HUD_ICON, false);
return getBoolean(BLUR_HUD_ICON, false);
}
public void markBlurHudIconTooltipSeen() {
store.beginWrite().putBoolean(BLUR_HUD_ICON, true).apply();
putBoolean(BLUR_HUD_ICON, true);
}
}

View File

@@ -1,24 +1,25 @@
package org.thoughtcrime.securesms.keyvalue;
public class UiHints {
import androidx.annotation.NonNull;
public class UiHints extends SignalStoreValues {
private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast";
private final KeyValueStore store;
UiHints(KeyValueStore store) {
this.store = store;
UiHints(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
markHasSeenGroupSettingsMenuToast();
}
public void markHasSeenGroupSettingsMenuToast() {
store.beginWrite().putBoolean(HAS_SEEN_GROUP_SETTINGS_MENU_TOAST, true).apply();
putBoolean(HAS_SEEN_GROUP_SETTINGS_MENU_TOAST, true);
}
public boolean hasSeenGroupSettingsMenuToast() {
return store.getBoolean(HAS_SEEN_GROUP_SETTINGS_MENU_TOAST, false);
return getBoolean(HAS_SEEN_GROUP_SETTINGS_MENU_TOAST, false);
}
}

View File

@@ -389,15 +389,15 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
if (showThread && (from.isLocalNumber() || thread.isGroup())) {
if (from.isLocalNumber()) {
return context.getString(R.string.MediaOverviewActivity_sent_by_you_to_s, thread.toShortString(context));
return context.getString(R.string.MediaOverviewActivity_sent_by_you_to_s, thread.getDisplayName(context));
} else {
return context.getString(R.string.MediaOverviewActivity_sent_by_s_to_s, from.toShortString(context), thread.toShortString(context));
return context.getString(R.string.MediaOverviewActivity_sent_by_s_to_s, from.getDisplayName(context), thread.getDisplayName(context));
}
} else {
if (from.isLocalNumber()) {
return context.getString(R.string.MediaOverviewActivity_sent_by_you);
} else {
return context.getString(R.string.MediaOverviewActivity_sent_by_s, from.toShortString(context));
return context.getString(R.string.MediaOverviewActivity_sent_by_s, from.getDisplayName(context));
}
}
}

View File

@@ -200,8 +200,8 @@ public final class MediaOverviewActivity extends PassphraseRequiredActionBarActi
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId),
(recipient) -> {
if (recipient != null) {
getSupportActionBar().setTitle(recipient.toShortString(this));
recipient.live().observe(this, r -> getSupportActionBar().setTitle(r.toShortString(this)));
getSupportActionBar().setTitle(recipient.getDisplayName(this));
recipient.live().observe(this, r -> getSupportActionBar().setTitle(r.getDisplayName(this)));
}
}
);

View File

@@ -718,6 +718,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
case ITEM_TOO_LARGE:
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
break;
case ONLY_ITEM_TOO_LARGE:
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
onNoMediaAvailable();
break;
case TOO_MANY_ITEMS:
int maxSelection = viewModel.getMaxSelection();
Toast.makeText(this, getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();

View File

@@ -147,7 +147,11 @@ class MediaSendViewModel extends ViewModel {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
if (filteredMedia.isEmpty() && newMedia.size() == 1 && page == Page.UNKNOWN) {
error.setValue(Error.ONLY_ITEM_TOO_LARGE);
} else {
error.setValue(Error.ITEM_TOO_LARGE);
}
}
if (filteredMedia.size() > maxSelection) {
@@ -670,7 +674,7 @@ class MediaSendViewModel extends ViewModel {
}
enum Error {
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS, ONLY_ITEM_TOO_LARGE
}
enum Event {

View File

@@ -220,8 +220,7 @@ public final class Megaphones {
}
private static boolean shouldShowMessageRequestsMegaphone() {
boolean userHasAProfileName = Recipient.self().getProfileName() != ProfileName.EMPTY;
return FeatureFlags.messageRequests() && !userHasAProfileName;
return Recipient.self().getProfileName() == ProfileName.EMPTY;
}
public enum Event {

View File

@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.messagedetails;
import androidx.annotation.NonNull;
import com.annimon.stream.ComparatorCompat;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
final class MessageDetails {
private static final Comparator<RecipientDeliveryStatus> HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()));
private static final Comparator<RecipientDeliveryStatus> ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication()));
private static final Comparator<RecipientDeliveryStatus> RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL);
private final MessageRecord messageRecord;
private final Collection<RecipientDeliveryStatus> pending;
private final Collection<RecipientDeliveryStatus> sent;
private final Collection<RecipientDeliveryStatus> delivered;
private final Collection<RecipientDeliveryStatus> read;
private final Collection<RecipientDeliveryStatus> notSent;
MessageDetails(MessageRecord messageRecord, List<RecipientDeliveryStatus> recipients) {
this.messageRecord = messageRecord;
pending = new TreeSet<>(RECIPIENT_COMPARATOR);
sent = new TreeSet<>(RECIPIENT_COMPARATOR);
delivered = new TreeSet<>(RECIPIENT_COMPARATOR);
read = new TreeSet<>(RECIPIENT_COMPARATOR);
notSent = new TreeSet<>(RECIPIENT_COMPARATOR);
if (messageRecord.isOutgoing()) {
for (RecipientDeliveryStatus status : recipients) {
switch (status.getDeliveryStatus()) {
case UNKNOWN:
notSent.add(status);
break;
case PENDING:
pending.add(status);
break;
case SENT:
sent.add(status);
break;
case DELIVERED:
delivered.add(status);
break;
case READ:
read.add(status);
break;
}
}
} else {
sent.addAll(recipients);
}
}
@NonNull MessageRecord getMessageRecord() {
return messageRecord;
}
@NonNull Collection<RecipientDeliveryStatus> getPending() {
return pending;
}
@NonNull Collection<RecipientDeliveryStatus> getSent() {
return sent;
}
@NonNull Collection<RecipientDeliveryStatus> getDelivered() {
return delivered;
}
@NonNull Collection<RecipientDeliveryStatus> getRead() {
return read;
}
@NonNull Collection<RecipientDeliveryStatus> getNotSent() {
return notSent;
}
}

View File

@@ -0,0 +1,161 @@
package org.thoughtcrime.securesms.messagedetails;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsAdapter.MessageDetailsViewState;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class MessageDetailsActivity extends PassphraseRequiredActionBarActivity {
private static final String MESSAGE_ID_EXTRA = "message_id";
private static final String THREAD_ID_EXTRA = "thread_id";
private static final String TYPE_EXTRA = "type";
private static final String RECIPIENT_EXTRA = "recipient_id";
private GlideRequests glideRequests;
private MessageDetailsViewModel viewModel;
private MessageDetailsAdapter adapter;
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
public static @NonNull Intent getIntentForMessageDetails(@NonNull Context context, @NonNull MessageRecord message, @NonNull RecipientId recipientId, long threadId) {
Intent intent = new Intent(context, MessageDetailsActivity.class);
intent.putExtra(MESSAGE_ID_EXTRA, message.getId());
intent.putExtra(THREAD_ID_EXTRA, threadId);
intent.putExtra(TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
intent.putExtra(RECIPIENT_EXTRA, recipientId);
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.message_details_activity);
glideRequests = GlideApp.with(this);
initializeList();
initializeViewModel();
initializeActionBar();
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
adapter.resumeMessageExpirationTimer();
}
@Override
protected void onPause() {
super.onPause();
adapter.pauseMessageExpirationTimer();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void initializeList() {
RecyclerView list = findViewById(R.id.message_details_list);
adapter = new MessageDetailsAdapter(glideRequests);
list.setAdapter(adapter);
list.setItemAnimator(null);
}
private void initializeViewModel() {
final RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
final String type = getIntent().getStringExtra(TYPE_EXTRA);
final Long messageId = getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1);
final Factory factory = new Factory(recipientId, type, messageId);
viewModel = ViewModelProviders.of(this, factory).get(MessageDetailsViewModel.class);
viewModel.getMessageDetails().observe(this, details -> {
if (details == null) {
finish();
} else {
adapter.submitList(convertToRows(details));
}
});
}
private void initializeActionBar() {
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
viewModel.getRecipientColor().observe(this, this::setActionBarColor);
}
private void setActionBarColor(MaterialColor color) {
assert getSupportActionBar() != null;
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color.toStatusBarColor(this));
}
}
private List<MessageDetailsViewState<?>> convertToRows(MessageDetails details) {
List<MessageDetailsViewState<?>> list = new ArrayList<>();
list.add(new MessageDetailsViewState<>(details.getMessageRecord(), MessageDetailsViewState.MESSAGE_HEADER));
if (details.getMessageRecord().isOutgoing()) {
addRecipients(list, RecipientHeader.NOT_SENT, details.getNotSent());
addRecipients(list, RecipientHeader.READ, details.getRead());
addRecipients(list, RecipientHeader.DELIVERED, details.getDelivered());
addRecipients(list, RecipientHeader.SENT_TO, details.getSent());
addRecipients(list, RecipientHeader.PENDING, details.getPending());
} else {
addRecipients(list, RecipientHeader.SENT_FROM, details.getSent());
}
return list;
}
private boolean addRecipients(List<MessageDetailsViewState<?>> list, RecipientHeader header, Collection<RecipientDeliveryStatus> recipients) {
if (recipients.isEmpty()) {
return false;
}
list.add(new MessageDetailsViewState<>(header, MessageDetailsViewState.RECIPIENT_HEADER));
for (RecipientDeliveryStatus status : recipients) {
list.add(new MessageDetailsViewState<>(status, MessageDetailsViewState.RECIPIENT));
}
return true;
}
}

View File

@@ -0,0 +1,140 @@
package org.thoughtcrime.securesms.messagedetails;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.List;
final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.MessageDetailsViewState<?>, RecyclerView.ViewHolder> {
private static final Object EXPIRATION_TIMER_CHANGE_PAYLOAD = new Object();
private final GlideRequests glideRequests;
private boolean running;
MessageDetailsAdapter(GlideRequests glideRequests) {
super(new MessageDetailsDiffer());
this.glideRequests = glideRequests;
running = true;
}
@Override
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case MessageDetailsViewState.MESSAGE_HEADER:
return new MessageHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_header, parent, false), glideRequests);
case MessageDetailsViewState.RECIPIENT_HEADER:
return new RecipientHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_recipient_header, parent, false));
case MessageDetailsViewState.RECIPIENT:
return new RecipientViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_recipient, parent, false));
default:
throw new AssertionError("unknown view type");
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof MessageHeaderViewHolder) {
((MessageHeaderViewHolder) holder).bind((MessageRecord) getItem(position).data, running);
} else if (holder instanceof RecipientHeaderViewHolder) {
((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data);
} else if (holder instanceof RecipientViewHolder) {
((RecipientViewHolder) holder).bind((RecipientDeliveryStatus) getItem(position).data);
} else {
throw new AssertionError("unknown view holder");
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else if (holder instanceof MessageHeaderViewHolder) {
((MessageHeaderViewHolder) holder).partialBind((MessageRecord) getItem(position).data, running);
}
}
@Override
public int getItemViewType(int position) {
return getItem(position).itemType;
}
void resumeMessageExpirationTimer() {
running = true;
if (getItemCount() > 0) {
notifyItemChanged(0, EXPIRATION_TIMER_CHANGE_PAYLOAD);
}
}
void pauseMessageExpirationTimer() {
running = false;
if (getItemCount() > 0) {
notifyItemChanged(0, EXPIRATION_TIMER_CHANGE_PAYLOAD);
}
}
private static class MessageDetailsDiffer extends DiffUtil.ItemCallback<MessageDetailsViewState<?>> {
@Override
public boolean areItemsTheSame(@NonNull MessageDetailsViewState<?> oldItem, @NonNull MessageDetailsViewState<?> newItem) {
Object oldData = oldItem.data;
Object newData = newItem.data;
if (oldData.getClass() == newData.getClass() && oldItem.itemType == newItem.itemType) {
switch (oldItem.itemType) {
case MessageDetailsViewState.MESSAGE_HEADER:
return true;
case MessageDetailsViewState.RECIPIENT_HEADER:
return oldData == newData;
case MessageDetailsViewState.RECIPIENT:
return ((RecipientDeliveryStatus) oldData).getRecipient().getId().equals(((RecipientDeliveryStatus) newData).getRecipient().getId());
}
}
return false;
}
@SuppressLint("DiffUtilEquals")
@Override
public boolean areContentsTheSame(@NonNull MessageDetailsViewState<?> oldItem, @NonNull MessageDetailsViewState<?> newItem) {
Object oldData = oldItem.data;
Object newData = newItem.data;
if (oldData.getClass() == newData.getClass() && oldItem.itemType == newItem.itemType) {
switch (oldItem.itemType) {
case MessageDetailsViewState.MESSAGE_HEADER:
return false;
case MessageDetailsViewState.RECIPIENT_HEADER:
return true;
case MessageDetailsViewState.RECIPIENT:
return ((RecipientDeliveryStatus) oldData).getDeliveryStatus() == ((RecipientDeliveryStatus) newData).getDeliveryStatus();
}
}
return false;
}
}
static final class MessageDetailsViewState<T> {
public static final int MESSAGE_HEADER = 0;
public static final int RECIPIENT_HEADER = 1;
public static final int RECIPIENT = 2;
private final T data;
private int itemType;
MessageDetailsViewState(T t, int itemType) {
this.data = t;
this.itemType = itemType;
}
}
}

View File

@@ -0,0 +1,132 @@
package org.thoughtcrime.securesms.messagedetails;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.LinkedList;
import java.util.List;
final class MessageDetailsRepository {
private final Context context = ApplicationDependencies.getApplication();
@NonNull LiveData<MessageRecord> getMessageRecord(String type, Long messageId) {
return new MessageRecordLiveData(context, type, messageId);
}
@NonNull LiveData<MessageDetails> getMessageDetails(@Nullable MessageRecord messageRecord) {
final MutableLiveData<MessageDetails> liveData = new MutableLiveData<>();
if (messageRecord != null) {
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getRecipientDeliveryStatusesInternal(messageRecord)));
} else {
liveData.setValue(null);
}
return liveData;
}
@WorkerThread
private @NonNull MessageDetails getRecipientDeliveryStatusesInternal(@NonNull MessageRecord messageRecord) {
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroup()) {
recipients.add(new RecipientDeliveryStatus(messageRecord,
messageRecord.getRecipient(),
getStatusFor(messageRecord),
messageRecord.isUnidentified(),
-1,
getNetworkFailure(messageRecord, messageRecord.getRecipient()),
getKeyMismatchFailure(messageRecord, messageRecord.getRecipient())));
} else {
List<GroupReceiptDatabase.GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(messageRecord,
recipient,
RecipientDeliveryStatus.Status.UNKNOWN,
false,
-1,
getNetworkFailure(messageRecord, recipient),
getKeyMismatchFailure(messageRecord, recipient)));
}
} else {
for (GroupReceiptDatabase.GroupReceiptInfo info : receiptInfoList) {
Recipient recipient = Recipient.resolved(info.getRecipientId());
NetworkFailure failure = getNetworkFailure(messageRecord, recipient);
IdentityKeyMismatch mismatch = getKeyMismatchFailure(messageRecord, recipient);
boolean recipientFailure = failure != null || mismatch != null;
recipients.add(new RecipientDeliveryStatus(messageRecord,
recipient,
getStatusFor(info.getStatus(), messageRecord.isPending(), recipientFailure),
info.isUnidentified(),
info.getTimestamp(),
failure,
mismatch));
}
}
}
return new MessageDetails(messageRecord, recipients);
}
private @Nullable NetworkFailure getNetworkFailure(MessageRecord messageRecord, Recipient recipient) {
if (messageRecord.hasNetworkFailures()) {
for (final NetworkFailure failure : messageRecord.getNetworkFailures()) {
if (failure.getRecipientId(context).equals(recipient.getId())) {
return failure;
}
}
}
return null;
}
private @Nullable IdentityKeyMismatch getKeyMismatchFailure(MessageRecord messageRecord, Recipient recipient) {
if (messageRecord.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : messageRecord.getIdentityKeyMismatches()) {
if (mismatch.getRecipientId(context).equals(recipient.getId())) {
return mismatch;
}
}
}
return null;
}
private @NonNull RecipientDeliveryStatus.Status getStatusFor(MessageRecord messageRecord) {
if (messageRecord.isRemoteRead()) return RecipientDeliveryStatus.Status.READ;
if (messageRecord.isDelivered()) return RecipientDeliveryStatus.Status.DELIVERED;
if (messageRecord.isSent()) return RecipientDeliveryStatus.Status.SENT;
if (messageRecord.isPending()) return RecipientDeliveryStatus.Status.PENDING;
return RecipientDeliveryStatus.Status.UNKNOWN;
}
private @NonNull RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
throw new AssertionError();
}
}

View File

@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.messagedetails;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Objects;
final class MessageDetailsViewModel extends ViewModel {
private final LiveData<Recipient> recipient;
private final LiveData<MessageDetails> messageDetails;
private MessageDetailsViewModel(RecipientId recipientId, String type, Long messageId) {
recipient = Recipient.live(recipientId).getLiveData();
MessageDetailsRepository repository = new MessageDetailsRepository();
LiveData<MessageRecord> messageRecord = repository.getMessageRecord(type, messageId);
messageDetails = Transformations.switchMap(messageRecord, repository::getMessageDetails);
}
@NonNull LiveData<MaterialColor> getRecipientColor() {
return Transformations.distinctUntilChanged(Transformations.map(recipient, Recipient::getColor));
}
@NonNull LiveData<MessageDetails> getMessageDetails() {
return messageDetails;
}
static final class Factory implements ViewModelProvider.Factory {
private final RecipientId recipientId;
private final String type;
private final Long messageId;
Factory(RecipientId recipientId, String type, Long messageId) {
this.recipientId = recipientId;
this.type = type;
this.messageId = messageId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new MessageDetailsViewModel(recipientId, type, messageId)));
}
}
}

View File

@@ -0,0 +1,210 @@
package org.thoughtcrime.securesms.messagedetails;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.view.View;
import android.view.ViewStub;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.Locale;
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
private final TextView sentDate;
private final TextView receivedDate;
private final TextView expiresIn;
private final TextView transport;
private final View expiresGroup;
private final View receivedGroup;
private final TextView errorText;
private final View resendButton;
private final View messageMetadata;
private final ViewStub updateStub;
private final ViewStub sentStub;
private final ViewStub receivedStub;
private GlideRequests glideRequests;
private ConversationItem conversationItem;
private ExpiresUpdater expiresUpdater;
MessageHeaderViewHolder(@NonNull View itemView, GlideRequests glideRequests) {
super(itemView);
this.glideRequests = glideRequests;
sentDate = itemView.findViewById(R.id.message_details_header_sent_time);
receivedDate = itemView.findViewById(R.id.message_details_header_received_time);
receivedGroup = itemView.findViewById(R.id.message_details_header_received_group);
expiresIn = itemView.findViewById(R.id.message_details_header_expires_in);
expiresGroup = itemView.findViewById(R.id.message_details_header_expires_group);
transport = itemView.findViewById(R.id.message_details_header_transport);
errorText = itemView.findViewById(R.id.message_details_header_error_text);
resendButton = itemView.findViewById(R.id.message_details_header_resend_button);
messageMetadata = itemView.findViewById(R.id.message_details_header_message_metadata);
updateStub = itemView.findViewById(R.id.message_details_header_message_view_update);
sentStub = itemView.findViewById(R.id.message_details_header_message_view_sent_multimedia);
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
}
void bind(MessageRecord messageRecord, boolean running) {
bindMessageView(messageRecord);
bindErrorState(messageRecord);
bindSentReceivedDates(messageRecord);
bindExpirationTime(messageRecord, running);
bindTransport(messageRecord);
}
void partialBind(MessageRecord messageRecord, boolean running) {
bindExpirationTime(messageRecord, running);
}
private void bindMessageView(MessageRecord messageRecord) {
if (conversationItem == null) {
if (messageRecord.isGroupAction()) conversationItem = (ConversationItem) updateStub.inflate();
else if (messageRecord.isOutgoing()) conversationItem = (ConversationItem) sentStub.inflate();
else conversationItem = (ConversationItem) receivedStub.inflate();
}
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false);
}
private void bindErrorState(MessageRecord messageRecord) {
boolean isPushGroup = messageRecord.getRecipient().isPushGroup();
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.VISIBLE);
resendButton.setOnClickListener(unused -> {
resendButton.setOnClickListener(null);
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(itemView.getContext().getApplicationContext(), messageRecord));
});
messageMetadata.setVisibility(View.GONE);
} else if (messageRecord.isFailed()) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
messageMetadata.setVisibility(View.GONE);
} else {
errorText.setVisibility(View.GONE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
messageMetadata.setVisibility(View.VISIBLE);
}
}
private void bindSentReceivedDates(MessageRecord messageRecord) {
sentDate.setOnLongClickListener(null);
receivedDate.setOnLongClickListener(null);
if (messageRecord.isPending() || messageRecord.isFailed()) {
sentDate.setText("-");
receivedGroup.setVisibility(View.GONE);
} else {
Locale dateLocale = Locale.getDefault();
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(itemView.getContext(), dateLocale);
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
sentDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
return true;
});
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
receivedDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
return true;
});
receivedGroup.setVisibility(View.VISIBLE);
} else {
receivedGroup.setVisibility(View.GONE);
}
}
}
private void bindExpirationTime(final MessageRecord messageRecord, boolean running) {
if (expiresUpdater != null) {
expiresUpdater.stop();
expiresUpdater = null;
}
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
expiresGroup.setVisibility(View.GONE);
return;
}
expiresGroup.setVisibility(View.VISIBLE);
if (running) {
expiresUpdater = new ExpiresUpdater(messageRecord);
Util.runOnMain(expiresUpdater);
}
}
private void bindTransport(MessageRecord messageRecord) {
final String transportText;
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = itemView.getContext().getString(R.string.ConversationFragment_pending);
} else if (messageRecord.isPush()) {
transportText = itemView.getContext().getString(R.string.ConversationFragment_push);
} else if (messageRecord.isMms()) {
transportText = itemView.getContext().getString(R.string.ConversationFragment_mms);
} else {
transportText = itemView.getContext().getString(R.string.ConversationFragment_sms);
}
transport.setText(transportText);
}
private void copyToClipboard(String text) {
((ClipboardManager) itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
}
private class ExpiresUpdater implements Runnable {
private final long expireStartedTimestamp;
private final long expiresInTimestamp;
private boolean running;
ExpiresUpdater(MessageRecord messageRecord) {
expireStartedTimestamp = messageRecord.getExpireStarted();
expiresInTimestamp = messageRecord.getExpiresIn();
running = true;
}
@Override
public void run() {
long elapsed = System.currentTimeMillis() - expireStartedTimestamp;
long remaining = expiresInTimestamp - elapsed;
int expirationTime = Math.max((int) (remaining / 1000), 1);
String duration = ExpirationUtil.getExpirationDisplayValue(itemView.getContext(), expirationTime);
expiresIn.setText(duration);
if (running && expirationTime > 1) {
Util.runOnMainDelayed(this, 500);
}
}
void stop() {
running = false;
}
}
}

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.messagedetails;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
final class MessageRecordLiveData extends LiveData<MessageRecord> {
private final Context context;
private final String type;
private final Long messageId;
private final ContentObserver obs;
private @Nullable Cursor cursor;
MessageRecordLiveData(Context context, String type, Long messageId) {
this.context = context;
this.type = type;
this.messageId = messageId;
obs = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
SignalExecutors.BOUNDED.execute(() -> resetCursor());
}
};
}
@Override
protected void onActive() {
retrieveMessageRecord();
}
@Override
protected void onInactive() {
SignalExecutors.BOUNDED.execute(this::destroyCursor);
}
private void retrieveMessageRecord() {
SignalExecutors.BOUNDED.execute(this::retrieveMessageRecordActual);
}
@WorkerThread
private synchronized void destroyCursor() {
if (cursor != null) {
cursor.unregisterContentObserver(obs);
cursor.close();
cursor = null;
}
}
@WorkerThread
private synchronized void resetCursor() {
destroyCursor();
retrieveMessageRecord();
}
@WorkerThread
private synchronized void retrieveMessageRecordActual() {
if (cursor != null) {
return;
}
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
handleSms();
break;
case MmsSmsDatabase.MMS_TRANSPORT:
handleMms();
break;
default:
throw new AssertionError("no valid message type specified");
}
}
@WorkerThread
private synchronized void handleSms() {
final SmsDatabase db = DatabaseFactory.getSmsDatabase(context);
final Cursor cursor = db.getVerboseMessageCursor(messageId);
final MessageRecord record = db.readerFor(cursor).getNext();
postValue(record);
cursor.registerContentObserver(obs);
this.cursor = cursor;
}
@WorkerThread
private synchronized void handleMms() {
final MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
final Cursor cursor = db.getVerboseMessage(messageId);
final MessageRecord record = db.readerFor(cursor).getNext();
postValue(record);
cursor.registerContentObserver(obs);
this.cursor = cursor;
}
}

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.messagedetails;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
final class RecipientDeliveryStatus {
enum Status {
UNKNOWN, PENDING, SENT, DELIVERED, READ
}
private final MessageRecord messageRecord;
private final Recipient recipient;
private final Status deliveryStatus;
private final boolean isUnidentified;
private final long timestamp;
private final NetworkFailure networkFailure;
private final IdentityKeyMismatch keyMismatchFailure;
RecipientDeliveryStatus(@NonNull MessageRecord messageRecord, @NonNull Recipient recipient, @NonNull Status deliveryStatus, boolean isUnidentified, long timestamp, @Nullable NetworkFailure networkFailure, @Nullable IdentityKeyMismatch keyMismatchFailure) {
this.messageRecord = messageRecord;
this.recipient = recipient;
this.deliveryStatus = deliveryStatus;
this.isUnidentified = isUnidentified;
this.timestamp = timestamp;
this.networkFailure = networkFailure;
this.keyMismatchFailure = keyMismatchFailure;
}
@NonNull MessageRecord getMessageRecord() {
return messageRecord;
}
@NonNull Status getDeliveryStatus() {
return deliveryStatus;
}
boolean isUnidentified() {
return isUnidentified;
}
long getTimestamp() {
return timestamp;
}
@NonNull Recipient getRecipient() {
return recipient;
}
@Nullable NetworkFailure getNetworkFailure() {
return networkFailure;
}
@Nullable IdentityKeyMismatch getKeyMismatchFailure() {
return keyMismatchFailure;
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.messagedetails;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
enum RecipientHeader {
PENDING(R.string.message_details_recipient_header__pending_send),
SENT_TO(R.string.message_details_recipient_header__sent_to),
SENT_FROM(R.string.message_details_recipient_header__sent_from),
DELIVERED(R.string.message_details_recipient_header__delivered_to),
READ(R.string.message_details_recipient_header__read_by),
NOT_SENT(R.string.message_details_recipient_header__not_sent);
private final int headerText;
RecipientHeader(@StringRes int headerText) {
this.headerText = headerText;
}
@StringRes int getHeaderText() {
return headerText;
}
}

View File

@@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.messagedetails;
import android.view.View;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
final class RecipientHeaderViewHolder extends RecyclerView.ViewHolder {
private final TextView header;
private final DeliveryStatusView deliveryStatus;
RecipientHeaderViewHolder(View itemView) {
super(itemView);
header = itemView.findViewById(R.id.recipient_header_text);
deliveryStatus = itemView.findViewById(R.id.recipient_header_delivery_status);
}
void bind(RecipientHeader recipientHeader) {
header.setText(recipientHeader.getHeaderText());
switch (recipientHeader) {
case PENDING:
deliveryStatus.setPending();
break;
case SENT_TO:
deliveryStatus.setSent();
break;
case DELIVERED:
deliveryStatus.setDelivered();
break;
case READ:
deliveryStatus.setRead();
break;
default:
deliveryStatus.setNone();
break;
}
}
}

Some files were not shown because too many files have changed in this diff Show More