mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-14 22:13:19 +01:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382ac7ba0d | ||
|
|
a46f47f352 | ||
|
|
e984d8a42c | ||
|
|
554bad6b8d | ||
|
|
ed13c97ad7 | ||
|
|
d33873d59a | ||
|
|
1234899ea1 | ||
|
|
13027dc44b | ||
|
|
5b4d74b7fe | ||
|
|
18c7bc2b5b | ||
|
|
bbbee0f372 | ||
|
|
cf9d090154 | ||
|
|
718471917f | ||
|
|
bb97407cde | ||
|
|
92ce678e29 | ||
|
|
e100aea2c7 | ||
|
|
fea3b6cb4a | ||
|
|
afbc132faa | ||
|
|
b27198286d | ||
|
|
ac93d81032 | ||
|
|
9981e5ca76 | ||
|
|
7dd3efeb53 | ||
|
|
d38d702adf | ||
|
|
04a000a8a8 | ||
|
|
3bbf0741ee | ||
|
|
e9a336100b | ||
|
|
fb600e9829 | ||
|
|
4a455ff958 | ||
|
|
707e238e5c | ||
|
|
90f22a4b66 | ||
|
|
b4f134adf7 | ||
|
|
1e00fc6149 | ||
|
|
f52133a69c | ||
|
|
91b142e0d9 | ||
|
|
26a9dd98c1 | ||
|
|
99e38e1d23 | ||
|
|
a2d8a25fd9 | ||
|
|
d86d625bcc | ||
|
|
18e3fb6609 | ||
|
|
da33ba0ed5 | ||
|
|
66f021d01a | ||
|
|
40231ea45f | ||
|
|
cd80a47c04 | ||
|
|
1033bd7bda | ||
|
|
b4f60f3acb | ||
|
|
bed3b571cc | ||
|
|
c8dd4e5254 | ||
|
|
514048171b | ||
|
|
32e9901592 | ||
|
|
d83f86a469 | ||
|
|
403d53586c | ||
|
|
6acae58694 | ||
|
|
a3f9737e63 | ||
|
|
263af7c139 | ||
|
|
7f2439f1e9 | ||
|
|
ae87d23003 | ||
|
|
3192cc0aac | ||
|
|
6102e9aa72 | ||
|
|
f4a152b0fe | ||
|
|
2b11bca7dc | ||
|
|
07d19f38e3 | ||
|
|
cd228c439e | ||
|
|
7a859c8961 | ||
|
|
543f38c75d | ||
|
|
f7b150f2d2 | ||
|
|
11328f643f | ||
|
|
f270a6b8c4 | ||
|
|
3fec23fd36 | ||
|
|
e01838e996 | ||
|
|
f70e41e7cd | ||
|
|
c4ec0c9897 | ||
|
|
989b071a6d | ||
|
|
c39751f9db | ||
|
|
dbf74a2234 | ||
|
|
837230d72d | ||
|
|
f544ec4126 | ||
|
|
79dbf85c1e | ||
|
|
61fe6cc961 | ||
|
|
70c88b68e2 | ||
|
|
d70c33d20f | ||
|
|
6b2e000e61 | ||
|
|
b9f11dafff | ||
|
|
9b32eaeb8a | ||
|
|
a99c0d438e | ||
|
|
c634c24afb | ||
|
|
2ddd1437cf | ||
|
|
9da309ca48 | ||
|
|
cfcd451db7 | ||
|
|
5ab72fd1a9 | ||
|
|
daace9bd1a | ||
|
|
69adcd1d69 | ||
|
|
0711a22188 | ||
|
|
3a06412cd8 | ||
|
|
51c82702e2 | ||
|
|
1b01196ec6 | ||
|
|
1cd6b58ece | ||
|
|
ea8e13b1c8 | ||
|
|
f392229393 | ||
|
|
a299bafe89 | ||
|
|
d2bf539504 | ||
|
|
903c3989b9 | ||
|
|
00996f0d7a | ||
|
|
4aded3a436 | ||
|
|
9acdc37729 | ||
|
|
d4cdcbe54f | ||
|
|
6fa2a0f411 | ||
|
|
558a8e4a14 | ||
|
|
8947b82034 | ||
|
|
56551025e9 | ||
|
|
befb4939d5 | ||
|
|
289f7aba63 | ||
|
|
28bd245b96 | ||
|
|
c5e7300df2 | ||
|
|
fe25d941bb | ||
|
|
4cda267f3b | ||
|
|
82ba7e2b8b | ||
|
|
41ebaf3938 | ||
|
|
090c400037 | ||
|
|
12b1232ac0 | ||
|
|
204a84c522 | ||
|
|
526afd539b | ||
|
|
d708984abd | ||
|
|
9d39db6428 | ||
|
|
67a8ec0d39 | ||
|
|
297a7d0ef8 | ||
|
|
4712833853 | ||
|
|
11d17f7496 | ||
|
|
36df3f234f | ||
|
|
098b298646 | ||
|
|
2f9320989a | ||
|
|
ec8d5defd4 | ||
|
|
981676c7f8 | ||
|
|
7c5ae57784 | ||
|
|
fc7be87468 | ||
|
|
e55d8007fc | ||
|
|
43b7aa2d52 | ||
|
|
cd1bad0718 | ||
|
|
6b47618351 | ||
|
|
b6d384120d | ||
|
|
1268b26c1f | ||
|
|
f1233bfddc | ||
|
|
1aa3e6afea | ||
|
|
ce21eb241a | ||
|
|
f96fb72eb1 | ||
|
|
207c467c6b | ||
|
|
9d1d9e33ed | ||
|
|
e4a76c0690 | ||
|
|
124c3e25e9 | ||
|
|
5cb1201903 | ||
|
|
bb6ca80d5a | ||
|
|
dc7c54a1f8 | ||
|
|
23401440bf | ||
|
|
f8f959e05a | ||
|
|
edbd4d2d03 | ||
|
|
a0b4065be3 | ||
|
|
1b2f964f32 |
@@ -59,9 +59,7 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2011 Whisper Systems
|
||||
|
||||
Copyright 2013-2020 Open Whisper Systems
|
||||
Copyright 2013-2020 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 635
|
||||
def canonicalVersionName = "4.60.4"
|
||||
def canonicalVersionCode = 649
|
||||
def canonicalVersionName = "4.62.1"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -126,7 +126,7 @@ android {
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
||||
@@ -161,7 +161,6 @@ android {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
exclude 'lib/*/libzkgroup.so' // TODO: GV2 Remove line to include .so when used
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -201,7 +200,7 @@ android {
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
@@ -282,9 +281,10 @@ dependencies {
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.paging:paging-common:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime:2.1.2"
|
||||
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
|
||||
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
|
||||
|
||||
|
||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
|
||||
@@ -258,6 +258,7 @@
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
@@ -346,12 +347,17 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MediaPreviewActivity"
|
||||
<activity android:name=".MediaPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".AvatarPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -422,7 +428,7 @@
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
@@ -486,13 +492,16 @@
|
||||
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
@@ -531,7 +540,9 @@
|
||||
|
||||
<service android:name=".service.GenericForegroundService"/>
|
||||
|
||||
<service android:name=".gcm.FcmService">
|
||||
<service android:name=".gcm.FcmFetchService" />
|
||||
|
||||
<service android:name=".gcm.FcmReceiveService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -50,8 +50,8 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
@@ -60,7 +60,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
@@ -153,6 +153,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
catchUpOnMessages();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,7 +161,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
isAppVisible = false;
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
MessageNotifier.setVisibleThread(-1);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
ApplicationDependencies.getFrameRateTracker().end();
|
||||
}
|
||||
|
||||
@@ -377,6 +378,36 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
});
|
||||
}
|
||||
|
||||
private void catchUpOnMessages() {
|
||||
InitialMessageRetriever retriever = ApplicationDependencies.getInitialMessageRetriever();
|
||||
|
||||
if (retriever.isCaughtUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
switch (retriever.begin(TimeUnit.SECONDS.toMillis(60))) {
|
||||
case SUCCESS:
|
||||
Log.i(TAG, "Successfully caught up on messages. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case FAILURE_TIMEOUT:
|
||||
Log.w(TAG, "Did not finish catching up due to a timeout. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case FAILURE_ERROR:
|
||||
Log.w(TAG, "Did not finish catching up due to an error. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case SKIPPED_ALREADY_CAUGHT_UP:
|
||||
Log.i(TAG, "Already caught up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case SKIPPED_ALREADY_RUNNING:
|
||||
Log.i(TAG, "Already in the process of catching up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
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 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.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
/**
|
||||
* Activity for displaying avatars full screen.
|
||||
*/
|
||||
public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final String TAG = Log.tag(AvatarPreviewActivity.class);
|
||||
|
||||
private static final String RECIPIENT_ID_EXTRA = "recipient_id";
|
||||
|
||||
public static @NonNull Intent intentFromRecipientId(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId)
|
||||
{
|
||||
Intent intent = new Intent(context, AvatarPreviewActivity.class);
|
||||
intent.putExtra(RECIPIENT_ID_EXTRA, recipientId.serialize());
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Bundle createTransitionBundle(@NonNull Activity activity, @NonNull View from) {
|
||||
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
setTheme(R.style.TextSecure_MediaPreview);
|
||||
setContentView(R.layout.contact_photo_preview_activity);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
showSystemUI();
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
|
||||
Recipient.live(recipientId).observe(this, 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();
|
||||
|
||||
GlideApp.with(this).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) {
|
||||
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(avatar);
|
||||
|
||||
toolbar.setTitle(recipient.toShortString(context));
|
||||
});
|
||||
|
||||
avatar.setOnClickListener(v -> toggleUiVisibility());
|
||||
|
||||
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
|
||||
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
||||
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||
|
||||
for (View view : views) {
|
||||
view.animate()
|
||||
.alpha(hide ? 0 : 1)
|
||||
.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleUiVisibility() {
|
||||
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
|
||||
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
hideSystemUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN );
|
||||
}
|
||||
|
||||
private void showSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import java.util.Set;
|
||||
|
||||
public interface BindableConversationListItem extends Unbindable {
|
||||
|
||||
public void bind(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
void bind(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,16 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextThemeWrapper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
public class ClearProfileAvatarActivity extends Activity {
|
||||
|
||||
private static final String ARG_TITLE = "arg_title";
|
||||
@@ -25,17 +33,17 @@ public class ClearProfileAvatarActivity extends Activity {
|
||||
|
||||
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(titleId)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
||||
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("delete", true);
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
})
|
||||
.setOnCancelListener(dialog -> finish())
|
||||
.show();
|
||||
new AlertDialog.Builder(new ContextThemeWrapper(this, DynamicTheme.isDarkTheme(this) ? R.style.TextSecure_DarkTheme : R.style.TextSecure_LightTheme))
|
||||
.setMessage(titleId)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
||||
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("delete", true);
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
})
|
||||
.setOnCancelListener(dialog -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,20 +19,18 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -46,14 +44,14 @@ import java.lang.ref.WeakReference;
|
||||
*/
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||
implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ContactSelectionListFragment.OnContactSelectedListener
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.ScrollCallback
|
||||
{
|
||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
protected ContactSelectionListFragment contactsFragment;
|
||||
|
||||
@@ -62,7 +60,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,7 +81,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
protected ContactFilterToolbar getToolbar() {
|
||||
@@ -95,7 +91,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
getSupportActionBar().setIcon(null);
|
||||
@@ -123,6 +118,17 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@Override
|
||||
public void onBeginScroll() {
|
||||
hideKeyboard();
|
||||
}
|
||||
|
||||
private void hideKeyboard() {
|
||||
ServiceUtil.getInputMethodManager(this)
|
||||
.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
|
||||
toolbar.clearFocus();
|
||||
}
|
||||
|
||||
private static class RefreshDirectoryTask extends AsyncTask<Context, Void, Void> {
|
||||
|
||||
private final WeakReference<ContactSelectionActivity> activity;
|
||||
|
||||
@@ -18,15 +18,18 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
@@ -35,14 +38,21 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
@@ -61,12 +71,10 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
@@ -76,6 +84,8 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
@@ -89,13 +99,19 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
||||
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||
|
||||
private final Debouncer scrollDebounce = new Debouncer(100);
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
@@ -109,11 +125,15 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private TextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private int selectionLimit;
|
||||
private Set<RecipientId> currentSelection;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -122,6 +142,10 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
if (context instanceof ListCallback) {
|
||||
listCallback = (ListCallback) context;
|
||||
}
|
||||
|
||||
if (context instanceof ScrollCallback) {
|
||||
scrollCallback = (ScrollCallback) context;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -163,26 +187,46 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||
|
||||
emptyText = ViewUtil.findById(view, android.R.id.empty);
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
|
||||
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
|
||||
emptyText = view.findViewById(android.R.id.empty);
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
swipeRefresh = view.findViewById(R.id.swipe_refresh);
|
||||
fastScroller = view.findViewById(R.id.fast_scroller);
|
||||
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipGroup = view.findViewById(R.id.chipGroup);
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
groupLimit = view.findViewById(R.id.group_limit);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
autoScrollOnNewItem();
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
if (selectionLimit != NO_LIMIT) {
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
||||
groupLimit.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupLimit.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -204,6 +248,13 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
}
|
||||
|
||||
private Set<RecipientId> getCurrentSelection() {
|
||||
List<RecipientId> currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
|
||||
|
||||
return currentSelection == null ? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
@@ -215,12 +266,17 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
glideRequests,
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti());
|
||||
isMulti(),
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
if (listCallback != null && FeatureFlags.newGroupUI()) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
|
||||
} else {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||
}
|
||||
headerAdapter.hide();
|
||||
concatenateAdapter.addAdapter(headerAdapter);
|
||||
}
|
||||
@@ -235,6 +291,16 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
|
||||
recyclerView.setAdapter(concatenateAdapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
||||
if (scrollCallback != null) {
|
||||
scrollCallback.onBeginScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private View createInviteActionView(@NonNull ListCallback listCallback) {
|
||||
@@ -247,7 +313,14 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
private View createNewGroupItem(@NonNull ListCallback listCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_new_group_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> listCallback.onNewGroup());
|
||||
view.setOnClickListener(v -> listCallback.onNewGroup(false));
|
||||
return view;
|
||||
}
|
||||
|
||||
private View createNewGroupsV1GroupItem(@NonNull ListCallback listCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_new_group_v1_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> listCallback.onNewGroup(true));
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -283,6 +356,10 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public boolean hasQueryFilter() {
|
||||
return !TextUtils.isEmpty(cursorFilter);
|
||||
}
|
||||
|
||||
public void setRefreshing(boolean refreshing) {
|
||||
swipeRefresh.setRefreshing(refreshing);
|
||||
}
|
||||
@@ -291,15 +368,16 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
cursorRecyclerViewAdapter.clearSelectedContacts();
|
||||
|
||||
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new ContactsCursorLoader(getActivity(),
|
||||
getActivity().getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
||||
cursorFilter, getActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
FragmentActivity activity = requireActivity();
|
||||
return new ContactsCursorLoader(activity,
|
||||
activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
||||
cursorFilter, activity.getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,7 +392,11 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
if (headerAdapter != null) {
|
||||
headerAdapter.show();
|
||||
if (TextUtils.isEmpty(cursorFilter)) {
|
||||
headerAdapter.show();
|
||||
} else {
|
||||
headerAdapter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||
@@ -382,6 +464,12 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionLimitReached()) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (contact.isUsernameType()) {
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
@@ -392,7 +480,8 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
markContactSelected(selected, contact);
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||
@@ -406,14 +495,16 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact, contact);
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
markContactUnselected(selectedContact, contact);
|
||||
markContactUnselected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
@@ -421,17 +512,20 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}}
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() >= selectionLimit;
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
listItem.setChecked(true);
|
||||
if (isMulti() && FeatureFlags.newGroupUI()) {
|
||||
chipGroup.addView(newChipForContact(listItem, selectedContact));
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
}
|
||||
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||
listItem.setChecked(false);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
removeChipForContact(selectedContact);
|
||||
}
|
||||
|
||||
@@ -442,28 +536,85 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
chipGroup.removeView(v);
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private View newChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) {
|
||||
final ContactChip chip = new ContactChip(requireContext());
|
||||
chip.setText(contact.getChipName());
|
||||
chip.setContact(selectedContact);
|
||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> addChipForRecipient(resolved, selectedContact));
|
||||
}
|
||||
|
||||
LiveRecipient recipient = contact.getRecipient();
|
||||
if (recipient != null) {
|
||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||
chip.setAvatar(glideRequests, resolved);
|
||||
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
||||
}
|
||||
);
|
||||
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
|
||||
final ContactChip chip = new ContactChip(requireContext());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||
}
|
||||
|
||||
chip.setText(recipient.getShortDisplayName(requireContext()));
|
||||
chip.setContact(selectedContact);
|
||||
chip.setCloseIconVisible(true);
|
||||
chip.setOnCloseIconClickListener(view -> {
|
||||
markContactUnselected(selectedContact, contact);
|
||||
chipGroup.removeView(chip);
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||
}
|
||||
});
|
||||
return chip;
|
||||
|
||||
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
||||
@Override
|
||||
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||
registerChipRecipientObserver(chip, recipient.live());
|
||||
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
|
||||
}
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
if (count < 0) throw new AssertionError();
|
||||
return count;
|
||||
}
|
||||
|
||||
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
||||
if (recipient != null) {
|
||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||
if (chip.isAttachedToWindow()) {
|
||||
chip.setAvatar(glideRequests, resolved, null);
|
||||
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(constraintLayout);
|
||||
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
|
||||
constraintSet.applyTo(constraintLayout);
|
||||
}
|
||||
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
@@ -474,14 +625,6 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
||||
}
|
||||
|
||||
private void autoScrollOnNewItem() {
|
||||
chipGroup.addOnLayoutChangeListener((view1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
if (right > oldRight) {
|
||||
scrollDebounce.publish(this::smoothScrollChipsToEnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void smoothScrollChipsToEnd() {
|
||||
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
|
||||
chipGroupScrollContainer.smoothScrollTo(x, 0);
|
||||
@@ -494,6 +637,10 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
|
||||
public interface ListCallback {
|
||||
void onInvite();
|
||||
void onNewGroup();
|
||||
void onNewGroup(boolean forceV1);
|
||||
}
|
||||
|
||||
public interface ScrollCallback {
|
||||
void onBeginScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,25 +164,29 @@ public class DeviceListFragment extends ListFragment
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDisconnectDevice(final long deviceId) {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
||||
R.string.DeviceListActivity_unlinking_device)
|
||||
new ProgressDialogAsyncTask<Void, Void, Boolean>(getActivity(),
|
||||
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
||||
R.string.DeviceListActivity_unlinking_device)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
accountManager.removeDevice(deviceId);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
if (result) {
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().hide();
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||
|
||||
@@ -56,8 +56,8 @@ public class MainNavigator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
|
||||
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition);
|
||||
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition, boolean highlightStartPosition) {
|
||||
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition, highlightStartPosition);
|
||||
|
||||
activity.startActivity(intent);
|
||||
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
||||
|
||||
@@ -32,7 +32,10 @@ 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;
|
||||
@@ -52,7 +55,6 @@ 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.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -133,13 +135,13 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
||||
|
||||
MessageNotifier.setVisibleThread(threadId);
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MessageNotifier.setVisibleThread(-1L);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -270,7 +272,9 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
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) {
|
||||
@@ -278,9 +282,9 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
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, itemParent, false);
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
|
||||
} else {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
|
||||
}
|
||||
itemParent.addView(conversationItem);
|
||||
}
|
||||
|
||||
@@ -81,23 +81,45 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
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)
|
||||
{
|
||||
if (this.member != null) this.member.getRecipient().live().removeForeverObserver(this);
|
||||
unsubscribeFromMember();
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.member = member;
|
||||
observeMember();
|
||||
|
||||
member.getRecipient().live().observeForever(this);
|
||||
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)
|
||||
{
|
||||
@@ -162,7 +184,7 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
if (this.member != null && this.member.getRecipient() != null) this.member.getRecipient().live().removeForeverObserver(this);
|
||||
unsubscribeFromMember();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,9 +19,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -121,7 +119,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewGroup() {
|
||||
public void onNewGroup(boolean forceV1) {
|
||||
handleCreateGroup();
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -45,16 +45,24 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
initializeToolbar();
|
||||
}
|
||||
|
||||
protected void initializeToolbar() {
|
||||
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
|
||||
getToolbar().setNavigationOnClickListener(v -> {
|
||||
Intent resultIntent = getIntent();
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||
|
||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
onFinishedSelection();
|
||||
});
|
||||
}
|
||||
|
||||
protected final void onFinishedSelection() {
|
||||
Intent resultIntent = getIntent();
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||
|
||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -35,7 +36,11 @@ 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;
|
||||
@@ -69,7 +74,6 @@ 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.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
@@ -100,8 +104,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
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 final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
|
||||
|
||||
private ImageView avatar;
|
||||
private GlideRequests glideRequests;
|
||||
@@ -120,7 +123,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,14 +137,13 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
setHeader(recipient.get());
|
||||
recipient.observe(this, this::setHeader);
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,10 +166,10 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbarLayout = ViewUtil.findById(this, R.id.collapsing_toolbar);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.threadPhotoRailView = ViewUtil.findById(this, R.id.recent_photos);
|
||||
this.threadPhotoRailLabel = ViewUtil.findById(this, R.id.rail_label);
|
||||
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));
|
||||
@@ -189,7 +190,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
}
|
||||
);
|
||||
|
||||
Toolbar toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setLogo(null);
|
||||
@@ -215,6 +216,20 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
.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);
|
||||
|
||||
@@ -203,8 +203,6 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
||||
viewModel.getBluetoothEnabled().observe(this, callScreen::setBluetoothEnabled);
|
||||
viewModel.getAudioOutput().observe(this, callScreen::setAudioOutput);
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
|
||||
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
|
||||
@@ -212,7 +210,6 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
|
||||
viewModel.isMoreThanOneCameraAvailable().observe(this, callScreen::showCameraToggleButton);
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
@@ -253,17 +250,23 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||
}
|
||||
|
||||
private void handleSetAudioSpeaker(boolean enabled) {
|
||||
private void handleSetAudioHandset() {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_SPEAKER, enabled);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth(boolean enabled) {
|
||||
private void handleSetAudioSpeaker() {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_SPEAKER);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_SPEAKER, true);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth() {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_BLUETOOTH, enabled);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_BLUETOOTH, true);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
@@ -546,13 +549,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
switch (audioOutput) {
|
||||
case HANDSET:
|
||||
handleSetAudioSpeaker(false);
|
||||
handleSetAudioHandset();
|
||||
break;
|
||||
case HEADSET:
|
||||
handleSetAudioBluetooth(true);
|
||||
handleSetAudioBluetooth();
|
||||
break;
|
||||
case SPEAKER:
|
||||
handleSetAudioSpeaker(true);
|
||||
handleSetAudioSpeaker();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown output: " + audioOutput);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
@@ -51,6 +53,9 @@ public abstract class Attachment {
|
||||
@Nullable
|
||||
private final BlurHash blurHash;
|
||||
|
||||
@Nullable
|
||||
private final AudioHash audioHash;
|
||||
|
||||
@NonNull
|
||||
private final TransformProperties transformProperties;
|
||||
|
||||
@@ -58,7 +63,7 @@ public abstract class Attachment {
|
||||
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
@@ -79,6 +84,7 @@ public abstract class Attachment {
|
||||
this.stickerLocator = stickerLocator;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.audioHash = audioHash;
|
||||
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
|
||||
}
|
||||
|
||||
@@ -172,6 +178,10 @@ public abstract class Attachment {
|
||||
return blurHash;
|
||||
}
|
||||
|
||||
public @Nullable AudioHash getAudioHash() {
|
||||
return audioHash;
|
||||
}
|
||||
|
||||
public @Nullable String getCaption() {
|
||||
return caption;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
@@ -25,11 +26,11 @@ public class DatabaseAttachment extends Attachment {
|
||||
String fileName, int cdnNumber, String location, String key, String relay,
|
||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties, int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -24,7 +25,7 @@ public class PointerAttachment extends Attachment {
|
||||
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null);
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -16,18 +17,18 @@ public class UriAttachment extends Attachment {
|
||||
|
||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, transformProperties);
|
||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
}
|
||||
|
||||
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
|
||||
@NonNull String contentType, int transferState, long size, int width, int height,
|
||||
@Nullable String fileName, @Nullable String fastPreflightId,
|
||||
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
||||
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An AudioHash is a compact string representation of the wave form and duration for an audio file.
|
||||
*/
|
||||
public final class AudioHash {
|
||||
|
||||
@NonNull private final String hash;
|
||||
@NonNull private final AudioWaveFormData audioWaveForm;
|
||||
|
||||
private AudioHash(@NonNull String hash, @NonNull AudioWaveFormData audioWaveForm) {
|
||||
this.hash = hash;
|
||||
this.audioWaveForm = audioWaveForm;
|
||||
}
|
||||
|
||||
public AudioHash(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
this(Base64.encodeBytes(audioWaveForm.toByteArray()), audioWaveForm);
|
||||
}
|
||||
|
||||
public static @Nullable AudioHash parseOrNull(@Nullable String hash) {
|
||||
if (hash == null) return null;
|
||||
try {
|
||||
return new AudioHash(hash, AudioWaveFormData.parseFrom(Base64.decode(hash)));
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull AudioWaveFormData getAudioWaveForm() {
|
||||
return audioWaveForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AudioHash other = (AudioHash) o;
|
||||
return hash.equals(other.hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash.hashCode();
|
||||
}
|
||||
|
||||
public @NonNull String getHash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaExtractor;
|
||||
import android.media.MediaFormat;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public final class AudioWaveForm {
|
||||
|
||||
private static final String TAG = Log.tag(AudioWaveForm.class);
|
||||
|
||||
private static final int BAR_COUNT = 46;
|
||||
private static final int SAMPLES_PER_BAR = 4;
|
||||
|
||||
private final Context context;
|
||||
private final AudioSlide slide;
|
||||
|
||||
public AudioWaveForm(@NonNull Context context, @NonNull AudioSlide slide) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.slide = slide;
|
||||
}
|
||||
|
||||
private static final LruCache<String, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
|
||||
private static final Executor AUDIO_DECODER_EXECUTOR = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
|
||||
@AnyThread
|
||||
public void getWaveForm(@NonNull Consumer<AudioFileInfo> onSuccess, @NonNull Runnable onFailure) {
|
||||
Uri uri = slide.getUri();
|
||||
Attachment attachment = slide.asAttachment();
|
||||
|
||||
if (uri == null) {
|
||||
Log.w(TAG, "No uri");
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attachment instanceof DatabaseAttachment)) {
|
||||
Log.i(TAG, "Not yet in database");
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = uri.toString();
|
||||
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cached != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache " + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(cached));
|
||||
return;
|
||||
}
|
||||
|
||||
AUDIO_DECODER_EXECUTOR.execute(() -> {
|
||||
AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cachedInExecutor != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(cachedInExecutor));
|
||||
return;
|
||||
}
|
||||
|
||||
AudioHash audioHash = attachment.getAudioHash();
|
||||
if (audioHash != null) {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
Log.i(TAG, "Loaded wave form from DB " + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(audioFileInfo));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
Util.runOnMain(onFailure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on decode sample from:
|
||||
* <p>
|
||||
* https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
|
||||
*/
|
||||
@WorkerThread
|
||||
@RequiresApi(api = 23)
|
||||
private @NonNull AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
|
||||
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
|
||||
long[] wave = new long[BAR_COUNT];
|
||||
int[] waveSamples = new int[BAR_COUNT];
|
||||
|
||||
MediaExtractor extractor = dataSource.createExtractor();
|
||||
|
||||
if (extractor.getTrackCount() == 0) {
|
||||
throw new IOException("No audio track");
|
||||
}
|
||||
|
||||
MediaFormat format = extractor.getTrackFormat(0);
|
||||
|
||||
if (!format.containsKey(MediaFormat.KEY_DURATION)) {
|
||||
throw new IOException("Unknown duration");
|
||||
}
|
||||
|
||||
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
|
||||
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||
|
||||
if (!mime.startsWith("audio/")) {
|
||||
throw new IOException("Mime not audio");
|
||||
}
|
||||
|
||||
MediaCodec codec = MediaCodec.createDecoderByType(mime);
|
||||
|
||||
if (totalDurationUs == 0) {
|
||||
throw new IOException("Zero duration");
|
||||
}
|
||||
|
||||
codec.configure(format, null, null, 0);
|
||||
codec.start();
|
||||
|
||||
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
|
||||
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
|
||||
|
||||
extractor.selectTrack(0);
|
||||
|
||||
long kTimeOutUs = 5000;
|
||||
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
||||
boolean sawInputEOS = false;
|
||||
boolean sawOutputEOS = false;
|
||||
int noOutputCounter = 0;
|
||||
|
||||
while (!sawOutputEOS && noOutputCounter < 50) {
|
||||
noOutputCounter++;
|
||||
if (!sawInputEOS) {
|
||||
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
|
||||
if (inputBufIndex >= 0) {
|
||||
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
|
||||
int sampleSize = extractor.readSampleData(dstBuf, 0);
|
||||
long presentationTimeUs = 0;
|
||||
|
||||
if (sampleSize < 0) {
|
||||
sawInputEOS = true;
|
||||
sampleSize = 0;
|
||||
} else {
|
||||
presentationTimeUs = extractor.getSampleTime();
|
||||
}
|
||||
|
||||
codec.queueInputBuffer(
|
||||
inputBufIndex,
|
||||
0,
|
||||
sampleSize,
|
||||
presentationTimeUs,
|
||||
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
|
||||
|
||||
if (!sawInputEOS) {
|
||||
int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||
sawInputEOS = !extractor.advance();
|
||||
int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||
while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
|
||||
sawInputEOS = !extractor.advance();
|
||||
if (!sawInputEOS) {
|
||||
nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int outputBufferIndex;
|
||||
do {
|
||||
outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
|
||||
if (outputBufferIndex >= 0) {
|
||||
if (info.size > 0) {
|
||||
noOutputCounter = 0;
|
||||
}
|
||||
|
||||
ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
|
||||
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
|
||||
long total = 0;
|
||||
for (int i = 0; i < info.size; i += 2 * 4) {
|
||||
short aShort = buf.getShort(i);
|
||||
total += Math.abs(aShort);
|
||||
}
|
||||
if (barIndex >= 0 && barIndex < wave.length) {
|
||||
wave[barIndex] += total;
|
||||
waveSamples[barIndex] += info.size / 2;
|
||||
}
|
||||
codec.releaseOutputBuffer(outputBufferIndex, false);
|
||||
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
sawOutputEOS = true;
|
||||
}
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
codecOutputBuffers = codec.getOutputBuffers();
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
|
||||
}
|
||||
} while (outputBufferIndex >= 0);
|
||||
}
|
||||
|
||||
codec.stop();
|
||||
codec.release();
|
||||
extractor.release();
|
||||
|
||||
float[] floats = new float[BAR_COUNT];
|
||||
byte[] bytes = new byte[BAR_COUNT];
|
||||
float max = 0;
|
||||
|
||||
for (int i = 0; i < BAR_COUNT; i++) {
|
||||
if (waveSamples[i] == 0) continue;
|
||||
|
||||
floats[i] = wave[i] / (float) waveSamples[i];
|
||||
if (floats[i] > max) {
|
||||
max = floats[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < BAR_COUNT; i++) {
|
||||
float normalized = floats[i] / max;
|
||||
bytes[i] = (byte) (255 * normalized);
|
||||
}
|
||||
|
||||
return new AudioFileInfo(totalDurationUs, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AudioFileInfo {
|
||||
private final long durationUs;
|
||||
private final byte[] waveFormBytes;
|
||||
private final float[] waveForm;
|
||||
|
||||
private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
|
||||
}
|
||||
|
||||
private AudioFileInfo(long durationUs, byte[] waveFormBytes) {
|
||||
this.durationUs = durationUs;
|
||||
this.waveFormBytes = waveFormBytes;
|
||||
this.waveForm = new float[waveFormBytes.length];
|
||||
|
||||
for (int i = 0; i < waveFormBytes.length; i++) {
|
||||
int unsigned = waveFormBytes[i] & 0xff;
|
||||
this.waveForm[i] = unsigned / 255f;
|
||||
}
|
||||
}
|
||||
|
||||
public long getDuration(@NonNull TimeUnit timeUnit) {
|
||||
return timeUnit.convert(durationUs, TimeUnit.MICROSECONDS);
|
||||
}
|
||||
|
||||
public float[] getWaveForm() {
|
||||
return waveForm;
|
||||
}
|
||||
|
||||
private @NonNull AudioWaveFormData toDatabaseProtobuf() {
|
||||
return AudioWaveFormData.newBuilder()
|
||||
.setDurationUs(durationUs)
|
||||
.setWaveForm(ByteString.copyFrom(waveFormBytes))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.color;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -8,12 +8,12 @@ import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.audio.AudioWaveForm;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -36,7 +37,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||
@@ -47,7 +48,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
private static final int REVERSE = -1;
|
||||
|
||||
@NonNull private final AnimatingToggle controlToggle;
|
||||
@NonNull private final ViewGroup container;
|
||||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
@@ -56,13 +56,17 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
private final boolean smallView;
|
||||
private final boolean autoRewind;
|
||||
|
||||
@Nullable private final TextView timestamp;
|
||||
@Nullable private final TextView duration;
|
||||
|
||||
@ColorInt private final int waveFormPlayedBarsColor;
|
||||
@ColorInt private final int waveFormUnplayedBarsColor;
|
||||
|
||||
@Nullable private SlideClickListener downloadListener;
|
||||
@Nullable private AudioSlidePlayer audioSlidePlayer;
|
||||
private int backwardsCounter;
|
||||
private int lottieDirection;
|
||||
private boolean isPlaying;
|
||||
private long durationMillis;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
@@ -83,22 +87,22 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
|
||||
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
||||
|
||||
this.container = findViewById(R.id.audio_widget_container);
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.circleProgress = findViewById(R.id.circle_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
this.duration = findViewById(R.id.duration);
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE),
|
||||
typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE));
|
||||
container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT));
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||
|
||||
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
|
||||
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||
} finally {
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
@@ -121,6 +125,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
public void setAudio(final @NonNull AudioSlide audio,
|
||||
final boolean showControls)
|
||||
{
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
if (audioSlidePlayer != null && !Objects.equals(audioSlidePlayer.getAudioSlide().getUri(), audio.getUri())) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setWaveMode(false);
|
||||
seekBar.setProgress(0);
|
||||
durationMillis = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (showControls && audio.isPendingDownload()) {
|
||||
controlToggle.displayQuick(downloadButton);
|
||||
@@ -141,6 +153,28 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
}
|
||||
|
||||
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
||||
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
new AudioWaveForm(getContext(), audio).getWaveForm(
|
||||
data -> {
|
||||
if (duration != null) {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
() -> waveFormView.setWaveMode(false));
|
||||
} else {
|
||||
waveFormView.setWaveMode(false);
|
||||
if (duration != null) {
|
||||
duration.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
@@ -210,10 +244,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
}
|
||||
|
||||
private void updateProgress(float progress, long millis) {
|
||||
if (timestamp != null) {
|
||||
timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis)));
|
||||
if (duration != null && durationMillis > 0) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
|
||||
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
if (smallView) {
|
||||
@@ -221,7 +254,7 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
}
|
||||
}
|
||||
|
||||
public void setTint(int foregroundTint, int backgroundTint) {
|
||||
public void setTint(int foregroundTint) {
|
||||
post(()-> this.playPauseButton.addValueCallback(new KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
|
||||
@@ -229,8 +262,8 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.circleProgress.setBarColor(foregroundTint);
|
||||
|
||||
if (this.timestamp != null) {
|
||||
this.timestamp.setTextColor(foregroundTint);
|
||||
if (this.duration != null) {
|
||||
this.duration.setTextColor(foregroundTint);
|
||||
}
|
||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
@@ -336,7 +369,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
||||
private boolean wasPlaying;
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser && durationMillis > 0) {
|
||||
float progressFloat = progress / (float) seekBar.getMax();
|
||||
updateProgress(progressFloat, (long) (durationMillis * progressFloat));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
@@ -21,8 +21,9 @@ import android.widget.LinearLayout;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
|
||||
|
||||
public class ContactFilterToolbar extends Toolbar {
|
||||
public class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
private OnFilterChangedListener listener;
|
||||
|
||||
private EditText searchText;
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.widget.AppCompatSeekBar;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class WaveFormSeekBarView extends AppCompatSeekBar {
|
||||
|
||||
private static final int ANIM_DURATION = 450;
|
||||
private static final int ANIM_BAR_OFF_SET_DURATION = 12;
|
||||
|
||||
private final Interpolator overshoot = new OvershootInterpolator();
|
||||
private final Paint paint = new Paint();
|
||||
private float[] data = new float[0];
|
||||
private long dataSetTime;
|
||||
private Drawable progressDrawable;
|
||||
private boolean waveMode;
|
||||
|
||||
@ColorInt private int playedBarColor = 0xffffffff;
|
||||
@ColorInt private int unplayedBarColor = 0x7fffffff;
|
||||
@Px private int barWidth;
|
||||
|
||||
public WaveFormSeekBarView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public WaveFormSeekBarView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public WaveFormSeekBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setWillNotDraw(false);
|
||||
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setAntiAlias(true);
|
||||
|
||||
progressDrawable = super.getProgressDrawable();
|
||||
|
||||
if (isInEditMode()) {
|
||||
setWaveData(sinusoidalExampleData());
|
||||
dataSetTime = 0;
|
||||
}
|
||||
|
||||
barWidth = getResources().getDimensionPixelSize(R.dimen.wave_form_bar_width);
|
||||
}
|
||||
|
||||
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor) {
|
||||
this.playedBarColor = playedBarColor;
|
||||
this.unplayedBarColor = unplayedBarColor;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgressDrawable(Drawable progressDrawable) {
|
||||
this.progressDrawable = progressDrawable;
|
||||
if (!waveMode) {
|
||||
super.setProgressDrawable(progressDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getProgressDrawable() {
|
||||
return progressDrawable;
|
||||
}
|
||||
|
||||
public void setWaveData(@NonNull float[] data) {
|
||||
if (!Arrays.equals(data, this.data)) {
|
||||
this.data = data;
|
||||
this.dataSetTime = System.currentTimeMillis();
|
||||
}
|
||||
setWaveMode(data.length > 0);
|
||||
}
|
||||
|
||||
public void setWaveMode(boolean waveMode) {
|
||||
this.waveMode = waveMode;
|
||||
super.setProgressDrawable(this.waveMode ? null : progressDrawable);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (waveMode) {
|
||||
drawWave(canvas);
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
private void drawWave(Canvas canvas) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
|
||||
int usableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
int usableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
float midpoint = usableHeight / 2f;
|
||||
float maxHeight = usableHeight / 2f - barWidth;
|
||||
float barGap = (usableWidth - data.length * barWidth) / (float) (data.length - 1);
|
||||
|
||||
boolean hasMoreFrames = false;
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||
|
||||
for (int bar = 0; bar < data.length; bar++) {
|
||||
float x = bar * (barWidth + barGap) + barWidth / 2f;
|
||||
float y = data[bar] * maxHeight;
|
||||
float progress = x / usableWidth;
|
||||
|
||||
paint.setColor(progress * getMax() < getProgress() ? playedBarColor : unplayedBarColor);
|
||||
|
||||
long time = System.currentTimeMillis() - bar * ANIM_BAR_OFF_SET_DURATION - dataSetTime;
|
||||
float timeX = Math.max(0, Math.min(1, time / (float) ANIM_DURATION));
|
||||
float interpolatedTime = overshoot.getInterpolation(timeX);
|
||||
float interpolatedY = y * interpolatedTime;
|
||||
|
||||
canvas.drawLine(x, midpoint - interpolatedY, x, midpoint + interpolatedY, paint);
|
||||
|
||||
if (time < ANIM_DURATION) {
|
||||
hasMoreFrames = true;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
if (hasMoreFrames) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] sinusoidalExampleData() {
|
||||
float[] data = new float[21];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = (float) Math.sin(i / (float) (data.length - 1) * 2 * Math.PI);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
{
|
||||
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||
|
||||
private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
|
||||
|
||||
private final Context context;
|
||||
private final List<EmojiPageModel> models;
|
||||
private final RecentEmojiPageModel recentModel;
|
||||
@@ -41,7 +43,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
this.context = context;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.models = new LinkedList<>();
|
||||
this.recentModel = new RecentEmojiPageModel(context);
|
||||
this.recentModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
|
||||
@@ -2,4 +2,10 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
public final class EmojiStrings {
|
||||
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
|
||||
public static final String PHOTO = "\uD83D\uDCF7";
|
||||
public static final String VIDEO = "\uD83C\uDFA5";
|
||||
public static final String GIF = "\uD83C\uDFA1";
|
||||
public static final String AUDIO = "\uD83C\uDFA4";
|
||||
public static final String FILE = "\uD83D\uDCCE";
|
||||
public static final String STICKER = "\u2B50";
|
||||
}
|
||||
|
||||
@@ -22,20 +22,21 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||
private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
private final String preferenceName;
|
||||
private final LinkedHashSet<String> recentlyUsed;
|
||||
|
||||
public RecentEmojiPageModel(Context context) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.recentlyUsed = getPersistedCache();
|
||||
public RecentEmojiPageModel(Context context, @NonNull String preferenceName) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.preferenceName = preferenceName;
|
||||
this.recentlyUsed = getPersistedCache();
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> getPersistedCache() {
|
||||
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
|
||||
String serialized = prefs.getString(preferenceName, "[]");
|
||||
try {
|
||||
CollectionType collectionType = TypeFactory.defaultInstance()
|
||||
.constructCollectionType(LinkedHashSet.class, String.class);
|
||||
@@ -90,7 +91,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(EMOJI_LRU_PREFERENCE, serialized)
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -12,26 +18,35 @@ import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.AudioOutputViewHolder> {
|
||||
final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.ViewHolder> {
|
||||
|
||||
private final Consumer<WebRtcAudioOutput> consumer;
|
||||
private final List<WebRtcAudioOutput> audioOutputs;
|
||||
private final OnAudioOutputChangedListener onAudioOutputChangedListener;
|
||||
private final List<WebRtcAudioOutput> audioOutputs;
|
||||
|
||||
AudioOutputAdapter(@NonNull Consumer<WebRtcAudioOutput> consumer, @NonNull List<WebRtcAudioOutput> audioOutputs) {
|
||||
this.audioOutputs = audioOutputs;
|
||||
this.consumer = consumer;
|
||||
private WebRtcAudioOutput selected;
|
||||
|
||||
AudioOutputAdapter(@NonNull OnAudioOutputChangedListener onAudioOutputChangedListener,
|
||||
@NonNull List<WebRtcAudioOutput> audioOutputs) {
|
||||
this.audioOutputs = audioOutputs;
|
||||
this.onAudioOutputChangedListener = onAudioOutputChangedListener;
|
||||
}
|
||||
|
||||
public void setSelectedOutput(@NonNull WebRtcAudioOutput selected) {
|
||||
this.selected = selected;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull AudioOutputViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new AudioOutputViewHolder((TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.audio_output_adapter_item, parent, false), consumer);
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.audio_output_adapter_radio_item, parent, false);
|
||||
|
||||
return new ViewHolder(view, this::handlePositionSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AudioOutputViewHolder holder, int position) {
|
||||
WebRtcAudioOutput audioOutput = audioOutputs.get(position);
|
||||
holder.view.setText(audioOutput.getLabelRes());
|
||||
holder.view.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(audioOutputs.get(position), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -39,21 +54,46 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.A
|
||||
return audioOutputs.size();
|
||||
}
|
||||
|
||||
final static class AudioOutputViewHolder extends RecyclerView.ViewHolder {
|
||||
private void handlePositionSelected(int position) {
|
||||
WebRtcAudioOutput mode = audioOutputs.get(position);
|
||||
|
||||
private final TextView view;
|
||||
|
||||
AudioOutputViewHolder(@NonNull TextView itemView, @NonNull Consumer<WebRtcAudioOutput> consumer) {
|
||||
super(itemView);
|
||||
|
||||
view = itemView;
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
consumer.accept(WebRtcAudioOutput.values()[getAdapterPosition()]);
|
||||
}
|
||||
});
|
||||
if (mode != selected) {
|
||||
setSelectedOutput(mode);
|
||||
onAudioOutputChangedListener.audioOutputChanged(selected);
|
||||
}
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
private final TextView textView;
|
||||
private final RadioButton radioButton;
|
||||
private final Consumer<Integer> onPressed;
|
||||
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onPressed) {
|
||||
super(itemView);
|
||||
|
||||
this.textView = itemView.findViewById(R.id.text);
|
||||
this.radioButton = itemView.findViewById(R.id.radio);
|
||||
this.onPressed = onPressed;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
void bind(@NonNull WebRtcAudioOutput audioOutput, @Nullable WebRtcAudioOutput selected) {
|
||||
textView.setText(audioOutput.getLabelRes());
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
|
||||
|
||||
radioButton.setOnCheckedChangeListener(null);
|
||||
radioButton.setChecked(audioOutput == selected);
|
||||
radioButton.setOnCheckedChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
int adapterPosition = getAdapterPosition();
|
||||
if (adapterPosition != RecyclerView.NO_POSITION) {
|
||||
onPressed.accept(adapterPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public interface OnAudioOutputChangedListener {
|
||||
void audioOutputChanged(WebRtcAudioOutput audioOutput);
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import androidx.annotation.StringRes;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public enum WebRtcAudioOutput {
|
||||
HANDSET(R.string.WebRtcAudioOutputToggle__phone, R.drawable.ic_phone_right_black_28),
|
||||
SPEAKER(R.string.WebRtcAudioOutputToggle__speaker, R.drawable.ic_speaker_solid_black_28),
|
||||
HEADSET(R.string.WebRtcAudioOutputToggle__bluetooth, R.drawable.ic_speaker_bt_solid_black_28);
|
||||
HANDSET(R.string.WebRtcAudioOutputToggle__phone_earpiece, R.drawable.ic_handset_solid_24),
|
||||
SPEAKER(R.string.WebRtcAudioOutputToggle__speaker, R.drawable.ic_speaker_solid_24),
|
||||
HEADSET(R.string.WebRtcAudioOutputToggle__bluetooth, R.drawable.ic_speaker_bt_solid_24);
|
||||
|
||||
private final @StringRes int labelRes;
|
||||
private final @DrawableRes int iconRes;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
@@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -21,37 +23,48 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
|
||||
private static final String STATE_OUTPUT_INDEX = "audio.output.toggle.state.output.index";
|
||||
private static final String STATE_HEADSET_ENABLED = "audio.output.toggle.state.headset.enabled";
|
||||
private static final String STATE_HANDSET_ENABLED = "audio.output.toggle.state.handset.enabled";
|
||||
private static final String STATE_PARENT = "audio.output.toggle.state.parent";
|
||||
|
||||
private static final int[] OUTPUT_HANDSET = { R.attr.state_handset };
|
||||
private static final int[] OUTPUT_SPEAKER = { R.attr.state_speaker };
|
||||
private static final int[] OUTPUT_HEADSET = { R.attr.state_headset };
|
||||
private static final int[][] OUTPUT_ENUM = { OUTPUT_HANDSET, OUTPUT_SPEAKER, OUTPUT_HEADSET };
|
||||
private static final List<WebRtcAudioOutput> OUTPUT_MODES = Arrays.asList(WebRtcAudioOutput.HANDSET, WebRtcAudioOutput.SPEAKER, WebRtcAudioOutput.HEADSET);
|
||||
private static final WebRtcAudioOutput OUTPUT_FALLBACK = WebRtcAudioOutput.HANDSET;
|
||||
private static final int[] SPEAKER_OFF = { R.attr.state_speaker_off };
|
||||
private static final int[] SPEAKER_ON = { R.attr.state_speaker_on };
|
||||
private static final int[] OUTPUT_HANDSET = { R.attr.state_handset_selected };
|
||||
private static final int[] OUTPUT_SPEAKER = { R.attr.state_speaker_selected };
|
||||
private static final int[] OUTPUT_HEADSET = { R.attr.state_headset_selected };
|
||||
private static final int[][] OUTPUT_ENUM = { SPEAKER_OFF, SPEAKER_ON, OUTPUT_HANDSET, OUTPUT_SPEAKER, OUTPUT_HEADSET };
|
||||
private static final List<WebRtcAudioOutput> OUTPUT_MODES = Arrays.asList(WebRtcAudioOutput.HANDSET, WebRtcAudioOutput.SPEAKER, WebRtcAudioOutput.HANDSET, WebRtcAudioOutput.SPEAKER, WebRtcAudioOutput.HEADSET);
|
||||
|
||||
private boolean isHeadsetAvailable;
|
||||
private boolean isHandsetAvailable;
|
||||
private int outputIndex;
|
||||
private OnAudioOutputChangedListener audioOutputChangedListener;
|
||||
private AlertDialog picker;
|
||||
private DialogInterface picker;
|
||||
|
||||
public WebRtcAudioOutputToggleButton(Context context) {
|
||||
public WebRtcAudioOutputToggleButton(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs) {
|
||||
public WebRtcAudioOutputToggleButton(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
public WebRtcAudioOutputToggleButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
super.setOnClickListener((v) -> {
|
||||
if (isHeadsetAvailable) showPicker();
|
||||
else setAudioOutput(OUTPUT_MODES.get((outputIndex + 1) % OUTPUT_ENUM.length));
|
||||
List<WebRtcAudioOutput> availableModes = buildOutputModeList(isHeadsetAvailable, isHandsetAvailable);
|
||||
|
||||
if (availableModes.size() > 2 || !isHandsetAvailable) showPicker(availableModes);
|
||||
else setAudioOutput(OUTPUT_MODES.get((outputIndex + 1) % OUTPUT_MODES.size()), true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
hidePicker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] extra = OUTPUT_ENUM[outputIndex];
|
||||
@@ -65,18 +78,21 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
throw new UnsupportedOperationException("This View does not support custom click listeners.");
|
||||
}
|
||||
|
||||
public void setIsHeadsetAvailable(boolean isHeadsetAvailable) {
|
||||
public void setControlAvailability(boolean isHandsetAvailable, boolean isHeadsetAvailable) {
|
||||
this.isHandsetAvailable = isHandsetAvailable;
|
||||
this.isHeadsetAvailable = isHeadsetAvailable;
|
||||
setAudioOutput(OUTPUT_MODES.get(outputIndex));
|
||||
}
|
||||
|
||||
public void setAudioOutput(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
public void setAudioOutput(@NonNull WebRtcAudioOutput audioOutput, boolean notifyListener) {
|
||||
int oldIndex = outputIndex;
|
||||
outputIndex = resolveAudioOutputIndex(OUTPUT_MODES.indexOf(audioOutput), isHeadsetAvailable);
|
||||
outputIndex = resolveAudioOutputIndex(OUTPUT_MODES.lastIndexOf(audioOutput));
|
||||
|
||||
if (oldIndex != outputIndex) {
|
||||
refreshDrawableState();
|
||||
notifyListener();
|
||||
|
||||
if (notifyListener) {
|
||||
notifyListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,23 +100,26 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
this.audioOutputChangedListener = listener;
|
||||
}
|
||||
|
||||
private void showPicker() {
|
||||
RecyclerView rv = new RecyclerView(getContext());
|
||||
private void showPicker(@NonNull List<WebRtcAudioOutput> availableModes) {
|
||||
RecyclerView rv = new RecyclerView(getContext());
|
||||
AudioOutputAdapter adapter = new AudioOutputAdapter(audioOutput -> {
|
||||
setAudioOutput(audioOutput, true);
|
||||
hidePicker();
|
||||
},
|
||||
availableModes);
|
||||
|
||||
adapter.setSelectedOutput(OUTPUT_MODES.get(outputIndex));
|
||||
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
|
||||
rv.setAdapter(new AudioOutputAdapter(this::setAudioOutputViaDialog, OUTPUT_MODES));
|
||||
rv.setAdapter(adapter);
|
||||
|
||||
picker = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.WebRtcAudioOutputToggle__audio_output)
|
||||
.setView(rv)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void hidePicker() {
|
||||
if (picker != null) {
|
||||
picker.dismiss();
|
||||
picker = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
Parcelable parentState = super.onSaveInstanceState();
|
||||
@@ -109,6 +128,7 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
bundle.putParcelable(STATE_PARENT, parentState);
|
||||
bundle.putInt(STATE_OUTPUT_INDEX, outputIndex);
|
||||
bundle.putBoolean(STATE_HEADSET_ENABLED, isHeadsetAvailable);
|
||||
bundle.putBoolean(STATE_HANDSET_ENABLED, isHandsetAvailable);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@@ -118,8 +138,11 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
Bundle savedState = (Bundle) state;
|
||||
|
||||
isHeadsetAvailable = savedState.getBoolean(STATE_HEADSET_ENABLED);
|
||||
isHandsetAvailable = savedState.getBoolean(STATE_HANDSET_ENABLED);
|
||||
|
||||
setAudioOutput(OUTPUT_MODES.get(
|
||||
resolveAudioOutputIndex(savedState.getInt(STATE_OUTPUT_INDEX), isHeadsetAvailable))
|
||||
resolveAudioOutputIndex(savedState.getInt(STATE_OUTPUT_INDEX))),
|
||||
false
|
||||
);
|
||||
|
||||
super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT));
|
||||
@@ -128,36 +151,60 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
private void hidePicker() {
|
||||
if (picker != null) {
|
||||
picker.dismiss();
|
||||
picker = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListener() {
|
||||
if (audioOutputChangedListener == null) return;
|
||||
|
||||
audioOutputChangedListener.audioOutputChanged(OUTPUT_MODES.get(outputIndex));
|
||||
}
|
||||
|
||||
private void setAudioOutputViaDialog(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
setAudioOutput(audioOutput);
|
||||
hidePicker();
|
||||
}
|
||||
private static List<WebRtcAudioOutput> buildOutputModeList(boolean isHeadsetAvailable, boolean isHandsetAvailable) {
|
||||
List<WebRtcAudioOutput> modes = new ArrayList(3);
|
||||
|
||||
private static int resolveAudioOutputIndex(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
|
||||
modes.add(WebRtcAudioOutput.SPEAKER);
|
||||
|
||||
if (isHeadsetAvailable) {
|
||||
modes.add(WebRtcAudioOutput.HEADSET);
|
||||
}
|
||||
|
||||
if (isHandsetAvailable) {
|
||||
modes.add(WebRtcAudioOutput.HANDSET);
|
||||
}
|
||||
|
||||
return modes;
|
||||
};
|
||||
|
||||
private int resolveAudioOutputIndex(int desiredAudioOutputIndex) {
|
||||
if (isIllegalAudioOutputIndex(desiredAudioOutputIndex)) {
|
||||
throw new IllegalArgumentException("Unsupported index: " + desiredAudioOutputIndex);
|
||||
}
|
||||
if (isUnsupportedAudioOutput(desiredAudioOutputIndex, isHeadsetAvailable)) {
|
||||
return OUTPUT_MODES.indexOf(OUTPUT_FALLBACK);
|
||||
if (isUnsupportedAudioOutput(desiredAudioOutputIndex, isHeadsetAvailable, isHandsetAvailable)) {
|
||||
if (!isHandsetAvailable) {
|
||||
return OUTPUT_MODES.lastIndexOf(WebRtcAudioOutput.SPEAKER);
|
||||
} else {
|
||||
return OUTPUT_MODES.indexOf(WebRtcAudioOutput.HANDSET);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isHeadsetAvailable) {
|
||||
return desiredAudioOutputIndex % 2;
|
||||
}
|
||||
|
||||
return desiredAudioOutputIndex;
|
||||
}
|
||||
|
||||
private static boolean isIllegalAudioOutputIndex(int desiredFlashIndex) {
|
||||
return desiredFlashIndex < 0 || desiredFlashIndex > OUTPUT_ENUM.length;
|
||||
private static boolean isIllegalAudioOutputIndex(int desiredAudioOutputIndex) {
|
||||
return desiredAudioOutputIndex < 0 || desiredAudioOutputIndex > OUTPUT_MODES.size();
|
||||
}
|
||||
|
||||
private static boolean isUnsupportedAudioOutput(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
|
||||
return OUTPUT_MODES.get(desiredAudioOutputIndex) == WebRtcAudioOutput.HEADSET && !isHeadsetAvailable;
|
||||
}
|
||||
|
||||
public interface OnAudioOutputChangedListener {
|
||||
void audioOutputChanged(WebRtcAudioOutput audioOutput);
|
||||
private static boolean isUnsupportedAudioOutput(int desiredAudioOutputIndex, boolean isHeadsetAvailable, boolean isHandsetAvailable) {
|
||||
return (OUTPUT_MODES.get(desiredAudioOutputIndex) == WebRtcAudioOutput.HEADSET && !isHeadsetAvailable) ||
|
||||
(OUTPUT_MODES.get(desiredAudioOutputIndex) == WebRtcAudioOutput.HANDSET && !isHandsetAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
@@ -29,7 +28,6 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -40,19 +38,20 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.webrtc.RendererCommon;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallView.class);
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
|
||||
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||
|
||||
public static final int FADE_OUT_DELAY = 5000;
|
||||
|
||||
private TextureViewRenderer localRenderer;
|
||||
private WebRtcAudioOutputToggleButton speakerToggle;
|
||||
private WebRtcAudioOutputToggleButton audioToggle;
|
||||
private AccessibleToggleButton videoToggle;
|
||||
private AccessibleToggleButton micToggle;
|
||||
private ViewGroup largeLocalRenderContainer;
|
||||
@@ -68,18 +67,21 @@ public class WebRtcCallView extends FrameLayout {
|
||||
private RecipientId recipientId;
|
||||
private CameraState.Direction cameraDirection;
|
||||
private ImageView answer;
|
||||
private View cameraDirectionToggle;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView hangup;
|
||||
private View answerWithAudio;
|
||||
private View answerWithAudioLabel;
|
||||
private View ongoingFooterGradient;
|
||||
|
||||
private Set<View> ongoingAudioCallViews;
|
||||
private Set<View> ongoingVideoCallViews;
|
||||
private Set<View> incomingAudioCallViews;
|
||||
private Set<View> incomingVideoCallViews;
|
||||
|
||||
private Set<View> currentVisibleViewSet = Collections.emptySet();
|
||||
private final Set<View> incomingCallViews = new HashSet<>();
|
||||
private final Set<View> topViews = new HashSet<>();
|
||||
private final Set<View> visibleViewSet = new HashSet<>();
|
||||
private final Set<View> adjustableMarginsSet = new HashSet<>();
|
||||
|
||||
private WebRtcControls controls = WebRtcControls.NONE;
|
||||
private final Runnable fadeOutRunnable = () -> { if (isAttachedToWindow() && shouldFadeControls(controls)) fadeOutControls(); };
|
||||
private final Runnable fadeOutRunnable = () -> {
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); };
|
||||
|
||||
public WebRtcCallView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
@@ -96,9 +98,9 @@ public class WebRtcCallView extends FrameLayout {
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
speakerToggle = findViewById(R.id.call_screen_speaker_toggle);
|
||||
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
|
||||
videoToggle = findViewById(R.id.call_screen_video_toggle);
|
||||
micToggle = findViewById(R.id.call_screen_mic_toggle);
|
||||
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
|
||||
localRenderPipFrame = findViewById(R.id.call_screen_pip);
|
||||
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder);
|
||||
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder);
|
||||
@@ -110,31 +112,34 @@ public class WebRtcCallView extends FrameLayout {
|
||||
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
|
||||
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View hangup = findViewById(R.id.call_screen_end_call);
|
||||
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
View answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
|
||||
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
|
||||
|
||||
Set<View> topAreaViews = Sets.newHashSet(status, topGradient, recipientName);
|
||||
topViews.add(status);
|
||||
topViews.add(topGradient);
|
||||
topViews.add(recipientName);
|
||||
|
||||
incomingAudioCallViews = Sets.newHashSet(decline, declineLabel, answer, answerLabel);
|
||||
incomingAudioCallViews.addAll(topAreaViews);
|
||||
incomingCallViews.add(answer);
|
||||
incomingCallViews.add(answerLabel);
|
||||
incomingCallViews.add(decline);
|
||||
incomingCallViews.add(declineLabel);
|
||||
incomingCallViews.add(incomingFooterGradient);
|
||||
|
||||
incomingVideoCallViews = Sets.newHashSet(decline, declineLabel, answer, answerLabel, answerWithAudio, answerWithAudioLabel);
|
||||
incomingVideoCallViews.addAll(topAreaViews);
|
||||
adjustableMarginsSet.add(micToggle);
|
||||
adjustableMarginsSet.add(cameraDirectionToggle);
|
||||
adjustableMarginsSet.add(videoToggle);
|
||||
adjustableMarginsSet.add(audioToggle);
|
||||
|
||||
ongoingAudioCallViews = Sets.newHashSet(micToggle, speakerToggle, videoToggle, hangup);
|
||||
ongoingAudioCallViews.addAll(topAreaViews);
|
||||
|
||||
ongoingVideoCallViews = Sets.newHashSet();
|
||||
ongoingVideoCallViews.addAll(ongoingAudioCallViews);
|
||||
|
||||
speakerToggle.setOnAudioOutputChangedListener(outputMode -> {
|
||||
audioToggle.setOnAudioOutputChangedListener(outputMode -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onAudioOutputChanged(outputMode));
|
||||
});
|
||||
|
||||
@@ -172,7 +177,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (shouldFadeControls(controls)) {
|
||||
if (controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
}
|
||||
}
|
||||
@@ -183,10 +188,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
cancelFadeOut();
|
||||
}
|
||||
|
||||
public void showCameraToggleButton(boolean shouldShowCameraToggleButton) {
|
||||
cameraDirectionToggle.setVisibility(shouldShowCameraToggleButton ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public void setControlsListener(@Nullable ControlsListener controlsListener) {
|
||||
this.controlsListener = controlsListener;
|
||||
}
|
||||
@@ -195,14 +196,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
|
||||
public void setBluetoothEnabled(boolean isBluetoothEnabled) {
|
||||
speakerToggle.setIsHeadsetAvailable(isBluetoothEnabled);
|
||||
}
|
||||
|
||||
public void setAudioOutput(WebRtcAudioOutput output) {
|
||||
speakerToggle.setAudioOutput(output);
|
||||
}
|
||||
|
||||
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
|
||||
if (isRemoteVideoEnabled) {
|
||||
remoteRenderContainer.setVisibility(View.VISIBLE);
|
||||
@@ -239,14 +232,12 @@ public class WebRtcCallView extends FrameLayout {
|
||||
case GONE:
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
cameraDirectionToggle.animate().setDuration(0).alpha(0f);
|
||||
setRenderer(largeLocalRenderContainer, null);
|
||||
setRenderer(smallLocalRenderContainer, null);
|
||||
break;
|
||||
case LARGE:
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.VISIBLE);
|
||||
cameraDirectionToggle.animate().setDuration(0).alpha(0f);
|
||||
if (largeLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(largeLocalRenderContainer, localRenderer);
|
||||
}
|
||||
@@ -254,9 +245,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
case SMALL:
|
||||
localRenderPipFrame.setVisibility(View.VISIBLE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
cameraDirectionToggle.animate()
|
||||
.setDuration(450)
|
||||
.alpha(1f);
|
||||
|
||||
if (smallLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(smallLocalRenderContainer, localRenderer);
|
||||
@@ -315,54 +303,71 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setWebRtcControls(WebRtcControls webRtcControls) {
|
||||
Set<View> lastVisibleSet = currentVisibleViewSet;
|
||||
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
|
||||
|
||||
Log.d(TAG, "Setting Controls: " + controls.name() + " -> " + webRtcControls.name());
|
||||
visibleViewSet.clear();
|
||||
visibleViewSet.addAll(topViews);
|
||||
|
||||
switch (webRtcControls) {
|
||||
case NONE:
|
||||
currentVisibleViewSet = Collections.emptySet();
|
||||
cancelFadeOut();
|
||||
break;
|
||||
case INCOMING_VIDEO:
|
||||
currentVisibleViewSet = incomingVideoCallViews;
|
||||
status.setText(R.string.WebRtcCallView__signal_video_call);
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video));
|
||||
cancelFadeOut();
|
||||
break;
|
||||
case INCOMING_AUDIO:
|
||||
currentVisibleViewSet = incomingAudioCallViews;
|
||||
status.setText(R.string.WebRtcCallView__signal_voice_call);
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer));
|
||||
cancelFadeOut();
|
||||
break;
|
||||
case ONGOING_LOCAL_AUDIO_REMOTE_AUDIO:
|
||||
currentVisibleViewSet = ongoingAudioCallViews;
|
||||
cancelFadeOut();
|
||||
break;
|
||||
case ONGOING_LOCAL_AUDIO_REMOTE_VIDEO:
|
||||
currentVisibleViewSet = ongoingAudioCallViews;
|
||||
if (!shouldFadeControls(controls)) {
|
||||
scheduleFadeOut();
|
||||
}
|
||||
break;
|
||||
case ONGOING_LOCAL_VIDEO_REMOTE_AUDIO:
|
||||
currentVisibleViewSet = ongoingVideoCallViews;
|
||||
cancelFadeOut();
|
||||
break;
|
||||
case ONGOING_LOCAL_VIDEO_REMOTE_VIDEO:
|
||||
currentVisibleViewSet = ongoingVideoCallViews;
|
||||
if (!shouldFadeControls(controls)) {
|
||||
scheduleFadeOut();
|
||||
}
|
||||
break;
|
||||
if (webRtcControls.displayIncomingCallButtons()) {
|
||||
visibleViewSet.addAll(incomingCallViews);
|
||||
|
||||
status.setText(R.string.WebRtcCallView__signal_voice_call);
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer));
|
||||
}
|
||||
|
||||
if (webRtcControls.displayAnswerWithAudio()) {
|
||||
visibleViewSet.add(answerWithAudio);
|
||||
visibleViewSet.add(answerWithAudioLabel);
|
||||
|
||||
status.setText(R.string.WebRtcCallView__signal_video_call);
|
||||
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video));
|
||||
}
|
||||
|
||||
if (webRtcControls.displayAudioToggle()) {
|
||||
visibleViewSet.add(audioToggle);
|
||||
|
||||
audioToggle.setControlAvailability(webRtcControls.enableHandsetInAudioToggle(),
|
||||
webRtcControls.enableHeadsetInAudioToggle());
|
||||
|
||||
audioToggle.setAudioOutput(webRtcControls.getAudioOutput(), false);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayCameraToggle()) {
|
||||
visibleViewSet.add(cameraDirectionToggle);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayEndCall()) {
|
||||
visibleViewSet.add(hangup);
|
||||
visibleViewSet.add(ongoingFooterGradient);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayMuteAudio()) {
|
||||
visibleViewSet.add(micToggle);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayVideoToggle()) {
|
||||
visibleViewSet.add(videoToggle);
|
||||
}
|
||||
|
||||
if (webRtcControls.displaySmallOngoingCallButtons()) {
|
||||
updateButtonStateForSmallButtons();
|
||||
} else if (webRtcControls.displayLargeOngoingCallButtons()) {
|
||||
updateButtonStateForLargeButtons();
|
||||
}
|
||||
|
||||
if (webRtcControls.isFadeOutEnabled()) {
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
}
|
||||
} else {
|
||||
cancelFadeOut();
|
||||
}
|
||||
|
||||
controls = webRtcControls;
|
||||
|
||||
if (!currentVisibleViewSet.equals(lastVisibleSet) || !shouldFadeControls(controls)) {
|
||||
fadeInNewUiState(lastVisibleSet);
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), speakerToggle.getTop()));
|
||||
if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) {
|
||||
fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons());
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +376,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
if (shouldFadeControls(controls) && status.getVisibility() == VISIBLE) {
|
||||
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) {
|
||||
fadeOutControls();
|
||||
} else {
|
||||
fadeInControls();
|
||||
@@ -386,7 +391,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private void fadeInControls() {
|
||||
fadeControls(ConstraintSet.VISIBLE);
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), speakerToggle.getTop());
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop());
|
||||
|
||||
scheduleFadeOut();
|
||||
}
|
||||
@@ -399,14 +404,14 @@ public class WebRtcCallView extends FrameLayout {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(parent);
|
||||
|
||||
for (View view : currentVisibleViewSet) {
|
||||
for (View view : visibleViewSet) {
|
||||
constraintSet.setVisibility(view.getId(), visibility);
|
||||
}
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
}
|
||||
|
||||
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet) {
|
||||
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
|
||||
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
|
||||
|
||||
TransitionManager.beginDelayedTransition(parent, transition);
|
||||
@@ -414,12 +419,19 @@ public class WebRtcCallView extends FrameLayout {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(parent);
|
||||
|
||||
for (View view : SetUtil.difference(previouslyVisibleViewSet, currentVisibleViewSet)) {
|
||||
for (View view : SetUtil.difference(previouslyVisibleViewSet, visibleViewSet)) {
|
||||
constraintSet.setVisibility(view.getId(), ConstraintSet.GONE);
|
||||
}
|
||||
|
||||
for (View view : currentVisibleViewSet) {
|
||||
for (View view : visibleViewSet) {
|
||||
constraintSet.setVisibility(view.getId(), ConstraintSet.VISIBLE);
|
||||
|
||||
if (adjustableMarginsSet.contains(view)) {
|
||||
constraintSet.setMargin(view.getId(),
|
||||
ConstraintSet.END,
|
||||
ViewUtil.dpToPx(useSmallMargins ? SMALL_ONGOING_CALL_BUTTON_MARGIN_DP
|
||||
: LARGE_ONGOING_CALL_BUTTON_MARGIN_DP));
|
||||
}
|
||||
}
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
@@ -477,8 +489,20 @@ public class WebRtcCallView extends FrameLayout {
|
||||
this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
||||
}
|
||||
|
||||
private static boolean shouldFadeControls(@NonNull WebRtcControls controls) {
|
||||
return controls == WebRtcControls.ONGOING_LOCAL_AUDIO_REMOTE_VIDEO || controls == WebRtcControls.ONGOING_LOCAL_VIDEO_REMOTE_VIDEO;
|
||||
private void updateButtonStateForLargeButtons() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
|
||||
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle);
|
||||
}
|
||||
|
||||
private void updateButtonStateForSmallButtons() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle_small);
|
||||
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup_small);
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
|
||||
@@ -20,14 +20,11 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<WebRtcAudioOutput> audioOutput = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> bluetoothEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
|
||||
private final MutableLiveData<Boolean> hasMultipleCameras = new MutableLiveData<>(false);
|
||||
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
|
||||
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
@@ -46,22 +43,10 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final WebRtcCallRepository repository = new WebRtcCallRepository();
|
||||
|
||||
public WebRtcCallViewModel() {
|
||||
audioOutput.setValue(repository.getAudioOutput());
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getRemoteVideoEnabled() {
|
||||
return Transformations.distinctUntilChanged(remoteVideoEnabled);
|
||||
}
|
||||
|
||||
public LiveData<WebRtcAudioOutput> getAudioOutput() {
|
||||
return Transformations.distinctUntilChanged(audioOutput);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getBluetoothEnabled() {
|
||||
return Transformations.distinctUntilChanged(bluetoothEnabled);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMicrophoneEnabled() {
|
||||
return Transformations.distinctUntilChanged(microphoneEnabled);
|
||||
}
|
||||
@@ -98,10 +83,6 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isMoreThanOneCameraAvailable() {
|
||||
return hasMultipleCameras;
|
||||
}
|
||||
|
||||
public boolean isAnswerWithVideoAvailable() {
|
||||
return answerWithVideoAvailable;
|
||||
}
|
||||
@@ -118,21 +99,21 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
@MainThread
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel) {
|
||||
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
|
||||
bluetoothEnabled.setValue(webRtcViewModel.isBluetoothAvailable());
|
||||
audioOutput.setValue(repository.getAudioOutput());
|
||||
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
|
||||
|
||||
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
|
||||
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
|
||||
}
|
||||
|
||||
hasMultipleCameras.setValue(webRtcViewModel.getLocalCameraState().getCameraCount() > 0);
|
||||
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
|
||||
updateLocalRenderState(webRtcViewModel.getState());
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getLocalCameraState().isEnabled(),
|
||||
webRtcViewModel.isRemoteVideoEnabled(),
|
||||
webRtcViewModel.isRemoteVideoOffer());
|
||||
webRtcViewModel.isRemoteVideoOffer(),
|
||||
webRtcViewModel.getLocalCameraState().getCameraCount() > 1,
|
||||
webRtcViewModel.isBluetoothAvailable(),
|
||||
repository.getAudioOutput());
|
||||
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = webRtcViewModel.getCallConnectedTime();
|
||||
@@ -167,23 +148,32 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWebRtcControls(WebRtcViewModel.State state, boolean isLocalVideoEnabled, boolean isRemoteVideoEnabled, boolean isRemoteVideoOffer) {
|
||||
private void updateWebRtcControls(WebRtcViewModel.State state,
|
||||
boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
|
||||
final WebRtcControls.CallState callState;
|
||||
|
||||
switch (state) {
|
||||
case CALL_INCOMING:
|
||||
webRtcControls.setValue(isRemoteVideoOffer ? WebRtcControls.INCOMING_VIDEO : WebRtcControls.INCOMING_AUDIO);
|
||||
callState = WebRtcControls.CallState.INCOMING;
|
||||
answerWithVideoAvailable = isRemoteVideoOffer;
|
||||
break;
|
||||
default:
|
||||
if (isLocalVideoEnabled && isRemoteVideoEnabled) {
|
||||
webRtcControls.setValue(WebRtcControls.ONGOING_LOCAL_VIDEO_REMOTE_VIDEO);
|
||||
} else if (isLocalVideoEnabled) {
|
||||
webRtcControls.setValue(WebRtcControls.ONGOING_LOCAL_VIDEO_REMOTE_AUDIO);
|
||||
} else if (isRemoteVideoEnabled) {
|
||||
webRtcControls.setValue(WebRtcControls.ONGOING_LOCAL_AUDIO_REMOTE_VIDEO);
|
||||
} else {
|
||||
webRtcControls.setValue(WebRtcControls.ONGOING_LOCAL_AUDIO_REMOTE_AUDIO);
|
||||
}
|
||||
callState = WebRtcControls.CallState.ONGOING;
|
||||
}
|
||||
|
||||
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
|
||||
isRemoteVideoEnabled || isRemoteVideoOffer,
|
||||
isMoreThanOneCameraAvailable,
|
||||
isBluetoothAvailable,
|
||||
callState,
|
||||
audioOutput));
|
||||
}
|
||||
|
||||
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
|
||||
|
||||
@@ -1,11 +1,100 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public enum WebRtcControls {
|
||||
NONE,
|
||||
ONGOING_LOCAL_AUDIO_REMOTE_AUDIO,
|
||||
ONGOING_LOCAL_AUDIO_REMOTE_VIDEO,
|
||||
ONGOING_LOCAL_VIDEO_REMOTE_AUDIO,
|
||||
ONGOING_LOCAL_VIDEO_REMOTE_VIDEO,
|
||||
INCOMING_AUDIO,
|
||||
INCOMING_VIDEO
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class WebRtcControls {
|
||||
|
||||
public static final WebRtcControls NONE = new WebRtcControls();
|
||||
|
||||
private final boolean isRemoteVideoEnabled;
|
||||
private final boolean isLocalVideoEnabled;
|
||||
private final boolean isMoreThanOneCameraAvailable;
|
||||
private final boolean isBluetoothAvailable;
|
||||
private final CallState callState;
|
||||
private final WebRtcAudioOutput audioOutput;
|
||||
|
||||
private WebRtcControls() {
|
||||
this(false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
|
||||
}
|
||||
|
||||
WebRtcControls(boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
@NonNull CallState callState,
|
||||
@NonNull WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
this.isLocalVideoEnabled = isLocalVideoEnabled;
|
||||
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
|
||||
this.isBluetoothAvailable = isBluetoothAvailable;
|
||||
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
|
||||
this.callState = callState;
|
||||
this.audioOutput = audioOutput;
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
return isOngoing();
|
||||
}
|
||||
|
||||
boolean displayMuteAudio() {
|
||||
return isOngoing();
|
||||
}
|
||||
|
||||
boolean displayVideoToggle() {
|
||||
return isOngoing();
|
||||
}
|
||||
|
||||
boolean displayAudioToggle() {
|
||||
return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
|
||||
}
|
||||
|
||||
boolean displayCameraToggle() {
|
||||
return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
|
||||
}
|
||||
|
||||
boolean displayAnswerWithAudio() {
|
||||
return isIncoming() && isRemoteVideoEnabled;
|
||||
}
|
||||
|
||||
boolean displayIncomingCallButtons() {
|
||||
return isIncoming();
|
||||
}
|
||||
|
||||
boolean enableHandsetInAudioToggle() {
|
||||
return !isLocalVideoEnabled;
|
||||
}
|
||||
|
||||
boolean enableHeadsetInAudioToggle() {
|
||||
return isBluetoothAvailable;
|
||||
}
|
||||
|
||||
boolean isFadeOutEnabled() {
|
||||
return isOngoing() && isRemoteVideoEnabled;
|
||||
}
|
||||
|
||||
boolean displaySmallOngoingCallButtons() {
|
||||
return isOngoing() && displayAudioToggle() && displayCameraToggle();
|
||||
}
|
||||
|
||||
boolean displayLargeOngoingCallButtons() {
|
||||
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
|
||||
}
|
||||
|
||||
WebRtcAudioOutput getAudioOutput() {
|
||||
return audioOutput;
|
||||
}
|
||||
|
||||
private boolean isOngoing() {
|
||||
return callState == CallState.ONGOING;
|
||||
}
|
||||
|
||||
private boolean isIncoming() {
|
||||
return callState == CallState.INCOMING;
|
||||
}
|
||||
|
||||
public enum CallState {
|
||||
NONE,
|
||||
INCOMING,
|
||||
ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,17 +44,21 @@ public final class ContactChip extends Chip {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient) {
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @Nullable Runnable onAvatarSet) {
|
||||
if (recipient != null) {
|
||||
requestManager.clear(this);
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = recipient.getFallbackContactPhotoDrawable(getContext(), false);
|
||||
Drawable fallbackContactPhotoDrawable = new HalfScaleDrawable(recipient.getFallbackContactPhotoDrawable(getContext(), false));
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
|
||||
if (contactPhoto == null) {
|
||||
setChipIcon(new HalfScaleDrawable(fallbackContactPhotoDrawable));
|
||||
setChipIcon(fallbackContactPhotoDrawable);
|
||||
if (onAvatarSet != null) {
|
||||
onAvatarSet.run();
|
||||
}
|
||||
} else {
|
||||
requestManager.load(contactPhoto)
|
||||
.placeholder(fallbackContactPhotoDrawable)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
@@ -63,6 +67,9 @@ public final class ContactChip extends Chip {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
setChipIcon(resource);
|
||||
if (onAvatarSet != null) {
|
||||
onAvatarSet.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* List adapter to display all contacts and their related information
|
||||
@@ -64,11 +65,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
|
||||
R.attr.contact_selection_lay_user};
|
||||
|
||||
public static final int PAYLOAD_SELECTION_CHANGE = 1;
|
||||
|
||||
private final boolean multiSelect;
|
||||
private final LayoutInflater li;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final TypedArray drawables;
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
|
||||
private final SelectedContactSet selectedContacts = new SelectedContactSet();
|
||||
|
||||
@@ -100,6 +104,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect);
|
||||
public abstract void unbind(@NonNull GlideRequests glideRequests);
|
||||
public abstract void setChecked(boolean checked);
|
||||
public abstract void setEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
public static class ContactViewHolder extends ViewHolder {
|
||||
@@ -129,6 +134,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
public void setChecked(boolean checked) {
|
||||
getView().setChecked(checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
getView().setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DividerViewHolder extends ViewHolder {
|
||||
@@ -150,6 +160,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
@@ -162,19 +175,22 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
boolean multiSelect)
|
||||
boolean multiSelect,
|
||||
@NonNull Set<RecipientId> currentContacts)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.li = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int i) {
|
||||
if (!isActiveCursor()) return -1;
|
||||
else if (i == -1) return -1;
|
||||
|
||||
int contactType = getContactType(i);
|
||||
|
||||
@@ -185,9 +201,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_CONTACT) {
|
||||
return new ContactViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
return new ContactViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
} else {
|
||||
return new DividerViewHolder(li.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
return new DividerViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +224,35 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
|
||||
viewHolder.unbind(glideRequests);
|
||||
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect);
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
if (currentContacts.contains(id)) {
|
||||
viewHolder.setChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor, @NonNull List<Object> payloads) {
|
||||
if (!arePayloadsValid(payloads)) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
|
||||
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
|
||||
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN));
|
||||
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
|
||||
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (currentContacts.contains(id)) {
|
||||
viewHolder.setChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
@@ -225,7 +268,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
|
||||
@@ -236,6 +278,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean arePayloadsValid(@NonNull List<Object> payloads) {
|
||||
return payloads.size() == 1 && payloads.get(0).equals(PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemViewRecycled(ViewHolder holder) {
|
||||
holder.unbind(glideRequests);
|
||||
|
||||
@@ -98,6 +98,12 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
||||
this.checkBox.setChecked(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
this.checkBox.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void unbind(GlideRequests glideRequests) {
|
||||
if (recipient != null) {
|
||||
recipient.removeForeverObserver(this);
|
||||
|
||||
@@ -115,52 +115,73 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
private List<Cursor> getUnfilteredResults() {
|
||||
ArrayList<Cursor> cursorList = new ArrayList<>();
|
||||
|
||||
if (recents) {
|
||||
Cursor recentConversations = getRecentConversationsCursor();
|
||||
if (recentConversations.getCount() > 0) {
|
||||
cursorList.add(getRecentsHeaderCursor());
|
||||
cursorList.add(recentConversations);
|
||||
cursorList.add(getContactsHeaderCursor());
|
||||
}
|
||||
}
|
||||
cursorList.addAll(getContactsCursors());
|
||||
addRecentsSection(cursorList);
|
||||
addContactsSection(cursorList);
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
private List<Cursor> getFilteredResults() {
|
||||
ArrayList<Cursor> cursorList = new ArrayList<>();
|
||||
|
||||
if (groupsEnabled(mode)) {
|
||||
Cursor groups = getGroupsCursor();
|
||||
if (groups.getCount() > 0) {
|
||||
List<Cursor> contacts = getContactsCursors();
|
||||
if (!isCursorListEmpty(contacts)) {
|
||||
cursorList.add(getContactsHeaderCursor());
|
||||
cursorList.addAll(contacts);
|
||||
cursorList.add(getGroupsHeaderCursor());
|
||||
}
|
||||
cursorList.add(groups);
|
||||
} else {
|
||||
cursorList.addAll(getContactsCursors());
|
||||
}
|
||||
} else {
|
||||
cursorList.addAll(getContactsCursors());
|
||||
addContactsSection(cursorList);
|
||||
addGroupsSection(cursorList);
|
||||
addNewNumberSection(cursorList);
|
||||
addUsernameSearchSection(cursorList);
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
private void addRecentsSection(@NonNull List<Cursor> cursorList) {
|
||||
if (!recents) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor recentConversations = getRecentConversationsCursor();
|
||||
|
||||
if (recentConversations.getCount() > 0) {
|
||||
cursorList.add(getRecentsHeaderCursor());
|
||||
cursorList.add(recentConversations);
|
||||
}
|
||||
}
|
||||
|
||||
private void addContactsSection(@NonNull List<Cursor> cursorList) {
|
||||
List<Cursor> contacts = getContactsCursors();
|
||||
|
||||
if (!isCursorListEmpty(contacts)) {
|
||||
cursorList.add(getContactsHeaderCursor());
|
||||
cursorList.addAll(getContactsCursors());
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroupsSection(@NonNull List<Cursor> cursorList) {
|
||||
if (!groupsEnabled(mode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor groups = getGroupsCursor();
|
||||
|
||||
if (groups.getCount() > 0) {
|
||||
cursorList.add(getGroupsHeaderCursor());
|
||||
cursorList.add(getGroupsCursor());
|
||||
}
|
||||
}
|
||||
|
||||
private void addNewNumberSection(@NonNull List<Cursor> cursorList) {
|
||||
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(filter)) {
|
||||
cursorList.add(getPhoneNumberSearchHeaderCursor());
|
||||
cursorList.add(getNewNumberCursor());
|
||||
} else if (!FeatureFlags.usernames() && NumberUtil.isValidSmsOrEmail(filter)){
|
||||
cursorList.add(getContactsHeaderCursor());
|
||||
cursorList.add(getPhoneNumberSearchHeaderCursor());
|
||||
cursorList.add(getNewNumberCursor());
|
||||
}
|
||||
}
|
||||
|
||||
private void addUsernameSearchSection(@NonNull List<Cursor> cursorList) {
|
||||
if (FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(filter)) {
|
||||
cursorList.add(getUsernameSearchHeaderCursor());
|
||||
cursorList.add(getUsernameSearchCursor());
|
||||
}
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
private Cursor getRecentsHeaderCursor() {
|
||||
@@ -279,7 +300,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
private Cursor getNewNumberCursor() {
|
||||
MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
|
||||
newNumberCursor.addRow(new Object[] { null,
|
||||
getContext().getString(R.string.contact_selection_list__unknown_contact),
|
||||
getUnknownContactTitle(),
|
||||
filter,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"\u21e2",
|
||||
@@ -290,7 +311,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
private Cursor getUsernameSearchCursor() {
|
||||
MatrixCursor cursor = new MatrixCursor(CONTACT_PROJECTION, 1);
|
||||
cursor.addRow(new Object[] { null,
|
||||
getContext().getString(R.string.contact_selection_list__unknown_contact),
|
||||
getUnknownContactTitle(),
|
||||
filter,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"\u21e2",
|
||||
@@ -298,6 +319,11 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private String getUnknownContactTitle() {
|
||||
return getContext().getString(newConversation(mode) ? R.string.contact_selection_list__unknown_contact
|
||||
: R.string.contact_selection_list__unknown_contact_add_to_group);
|
||||
}
|
||||
|
||||
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {
|
||||
try {
|
||||
final long startMillis = System.currentTimeMillis();
|
||||
@@ -334,6 +360,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
return flagSet(mode, DisplayMode.FLAG_SELF);
|
||||
}
|
||||
|
||||
private static boolean newConversation(int mode) {
|
||||
return groupsEnabled(mode);
|
||||
}
|
||||
|
||||
private static boolean pushEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_PUSH);
|
||||
}
|
||||
|
||||
@@ -43,13 +43,19 @@ public final class SelectedContact {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff any non-null property matches one on the other contact.
|
||||
* Returns true when non-null recipient ids match, and false if not.
|
||||
* <p>
|
||||
* If one or more recipient id is not set, then it returns true iff any other non-null property
|
||||
* matches one on the other contact.
|
||||
*/
|
||||
public boolean matches(@Nullable SelectedContact other) {
|
||||
if (other == null) return false;
|
||||
|
||||
return recipientId != null && recipientId.equals(other.recipientId) ||
|
||||
number != null && number .equals(other.number) ||
|
||||
username != null && username .equals(other.username);
|
||||
if (recipientId != null && other.recipientId != null) {
|
||||
return recipientId.equals(other.recipientId);
|
||||
}
|
||||
|
||||
return number != null && number .equals(other.number) ||
|
||||
username != null && username.equals(other.username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
@@ -178,9 +177,9 @@ class DirectoryHelperV1 {
|
||||
if (insertResult.isPresent()) {
|
||||
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
|
||||
if (hour >= 9 && hour < 23) {
|
||||
MessageNotifier.updateNotification(context, insertResult.get().getThreadId(), true);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
|
||||
} else {
|
||||
MessageNotifier.updateNotification(context, insertResult.get().getThreadId(), false);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ public class Contact implements Parcelable {
|
||||
|
||||
private static Attachment attachmentFromUri(@Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null);
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -43,6 +43,7 @@ import android.provider.Telephony;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -125,6 +126,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -157,13 +159,13 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
@@ -194,7 +196,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
@@ -281,13 +282,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
public static final String MEDIA_EXTRA = "media_list";
|
||||
public static final String STICKER_EXTRA = "sticker_extra";
|
||||
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
|
||||
public static final String STARTING_POSITION_EXTRA = "starting_position";
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
public static final String MEDIA_EXTRA = "media_list";
|
||||
public static final String STICKER_EXTRA = "sticker_extra";
|
||||
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
|
||||
public static final String STARTING_POSITION_EXTRA = "starting_position";
|
||||
public static final String HIGHLIGHT_STARTING_POSITION_EXTRA = "highlight_starting_position";
|
||||
|
||||
private static final int PICK_GALLERY = 1;
|
||||
private static final int PICK_DOCUMENT = 2;
|
||||
@@ -338,6 +340,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private ConversationStickerViewModel stickerViewModel;
|
||||
private ConversationViewModel viewModel;
|
||||
private InviteReminderModel inviteReminderModel;
|
||||
private ConversationGroupViewModel groupViewModel;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
@@ -355,13 +358,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
int distributionType,
|
||||
int startingPosition)
|
||||
int startingPosition,
|
||||
boolean highlightStartingPosition)
|
||||
{
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
|
||||
intent.putExtra(ConversationActivity.HIGHLIGHT_STARTING_POSITION_EXTRA, highlightStartingPosition);
|
||||
|
||||
return intent;
|
||||
}
|
||||
@@ -403,6 +408,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeSearchObserver();
|
||||
initializeStickerObserver();
|
||||
initializeViewModel();
|
||||
initializeGroupViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -482,7 +489,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
initializeEnabledCheck();
|
||||
initializeMmsEnabledCheck();
|
||||
initializeIdentityRecords();
|
||||
composeText.setTransport(sendButton.getSelectedTransport());
|
||||
@@ -499,14 +505,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(recipientSnapshot.getGroupId().get().requireV2()));
|
||||
}
|
||||
|
||||
MessageNotifier.setVisibleThread(threadId);
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
markThreadAsRead();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MessageNotifier.setVisibleThread(-1L);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_end);
|
||||
inputPanel.onPause();
|
||||
|
||||
@@ -701,8 +707,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
GroupActiveState groupActiveState = groupViewModel.getGroupActiveState().getValue();
|
||||
boolean isActiveGroup = groupActiveState != null && groupActiveState.isActiveGroup();
|
||||
boolean isActiveV2Group = groupActiveState != null && groupActiveState.isActiveV2Group();
|
||||
|
||||
if (isInMessageRequest()) {
|
||||
if (isActiveGroup()) {
|
||||
if (isActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_message_requests_group, menu);
|
||||
}
|
||||
|
||||
@@ -738,9 +748,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
} else {
|
||||
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
|
||||
}
|
||||
} else if (isActiveV2Group()) {
|
||||
} else if (isActiveV2Group || isActiveGroup && FeatureFlags.newGroupUI()) {
|
||||
inflater.inflate(R.menu.conversation_push_group_v2_options, menu);
|
||||
} else if (isActiveGroup()) {
|
||||
} else if (isActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_push_group_options, menu);
|
||||
}
|
||||
}
|
||||
@@ -753,8 +763,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inflater.inflate(R.menu.conversation_insecure, menu);
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
|
||||
else inflater.inflate(R.menu.conversation_unmuted, menu);
|
||||
if (!FeatureFlags.newGroupUI()) {
|
||||
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
|
||||
else inflater.inflate(R.menu.conversation_unmuted, menu);
|
||||
}
|
||||
|
||||
if (isSingleConversation() && getRecipient().getContactUri() == null) {
|
||||
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
|
||||
@@ -784,6 +796,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
}
|
||||
|
||||
if (isActiveV2Group) {
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings);
|
||||
} else if (isActiveGroup) {
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings);
|
||||
}
|
||||
|
||||
searchViewItem = menu.findItem(R.id.menu_search);
|
||||
|
||||
SearchView searchView = (SearchView) searchViewItem.getActionView();
|
||||
@@ -855,8 +874,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
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_manage_group: handleManagePushGroup(); return true;
|
||||
case R.id.menu_pending_members: handlePendingMembers(); return true;
|
||||
case R.id.menu_group_settings: handleManagePushGroup(); 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;
|
||||
@@ -870,6 +888,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuOpened(int featureId, Menu menu) {
|
||||
if (menu == null) {
|
||||
return super.onMenuOpened(featureId, null);
|
||||
}
|
||||
|
||||
if (!SignalStore.uiHints().hasSeenGroupSettingsMenuToast()) {
|
||||
MenuItem settingsMenuItem = menu.findItem(R.id.menu_group_settings);
|
||||
|
||||
if (settingsMenuItem != null && settingsMenuItem.isVisible()) {
|
||||
Toast toast = Toast.makeText(this, R.string.ConversationActivity__more_options_now_in_group_settings, Toast.LENGTH_SHORT);
|
||||
|
||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
||||
toast.show();
|
||||
|
||||
SignalStore.uiHints().markHasSeenGroupSettingsMenuToast();
|
||||
}
|
||||
}
|
||||
|
||||
return super.onMenuOpened(featureId, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
Log.d(TAG, "onBackPressed()");
|
||||
@@ -991,6 +1031,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void handleConversationSettings() {
|
||||
if (FeatureFlags.newGroupUI() && isPushGroupConversation()) {
|
||||
startActivitySceneTransition(ManageGroupActivity.newIntent(this, getRecipient().requireGroupId().requirePush()),
|
||||
titleView.findViewById(R.id.contact_photo_image),
|
||||
"avatar");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInMessageRequest()) return;
|
||||
|
||||
Intent intent = RecipientPreferenceActivity.getLaunchIntent(this, recipient.getId());
|
||||
@@ -1147,7 +1194,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
LeaveGroupDialog.handleLeavePushGroup(ConversationActivity.this,
|
||||
getLifecycle(),
|
||||
getRecipient().requireGroupId().requirePush(),
|
||||
this::initializeEnabledCheck);
|
||||
null);
|
||||
}
|
||||
|
||||
private void handleEditPushGroupV1() {
|
||||
@@ -1155,11 +1202,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void handleManagePushGroup() {
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()), GROUP_EDIT);
|
||||
}
|
||||
|
||||
private void handlePendingMembers() {
|
||||
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requirePush()), GROUP_EDIT);
|
||||
}
|
||||
|
||||
private void handleDistributionBroadcastEnabled(MenuItem item) {
|
||||
@@ -1381,10 +1424,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void initializeEnabledCheck() {
|
||||
boolean enabled = !(isPushGroupConversation() && !isActiveGroup());
|
||||
inputPanel.setEnabled(enabled);
|
||||
sendButton.setEnabled(enabled);
|
||||
attachButton.setEnabled(enabled);
|
||||
groupViewModel.getGroupActiveState().observe(this, state -> {
|
||||
boolean enabled = state == null || !(isPushGroupConversation() && !state.isActiveGroup());
|
||||
inputPanel.setEnabled(enabled);
|
||||
sendButton.setEnabled(enabled);
|
||||
attachButton.setEnabled(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
@@ -1815,6 +1860,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
this.viewModel = ViewModelProviders.of(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeGroupViewModel() {
|
||||
groupViewModel = ViewModelProviders.of(this, new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
|
||||
recipient.observe(this, groupViewModel::onRecipientChange);
|
||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||
}
|
||||
|
||||
private void showStickerIntroductionTooltip() {
|
||||
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
||||
inputPanel.setMediaKeyboardToggleMode(true);
|
||||
@@ -1912,6 +1963,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (searchViewItem == null || !searchViewItem.isActionViewExpanded()) {
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
if (groupViewModel != null) {
|
||||
groupViewModel.onRecipientChange(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@@ -2169,13 +2224,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return record.isPresent() && record.get().isActive();
|
||||
}
|
||||
|
||||
private boolean isActiveV2Group() {
|
||||
if (!isGroupConversation()) return false;
|
||||
|
||||
Optional<GroupRecord> record = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getId());
|
||||
return record.isPresent() && record.get().isActive() && record.get().isV2Group();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifiableIfStatement")
|
||||
private boolean isSelfConversation() {
|
||||
if (!TextSecurePreferences.isPushRegistered(this)) return false;
|
||||
@@ -2230,7 +2278,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
Context context = ConversationActivity.this;
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false);
|
||||
|
||||
MessageNotifier.updateNotification(context);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
return null;
|
||||
@@ -2260,7 +2308,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
if (refreshFragment) {
|
||||
fragment.reload(recipient.get(), threadId);
|
||||
MessageNotifier.setVisibleThread(threadId);
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
}
|
||||
|
||||
fragment.scrollToBottom();
|
||||
|
||||
@@ -72,12 +72,14 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
private static final String TAG = Log.tag(ConversationAdapter.class);
|
||||
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 0;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 1;
|
||||
private static final int MESSAGE_TYPE_UPDATE = 2;
|
||||
private static final int MESSAGE_TYPE_HEADER = 3;
|
||||
private static final int MESSAGE_TYPE_FOOTER = 4;
|
||||
private static final int MESSAGE_TYPE_PLACEHOLDER = 5;
|
||||
private static final int MESSAGE_TYPE_OUTGOING_MULTIMEDIA = 0;
|
||||
private static final int MESSAGE_TYPE_OUTGOING_TEXT = 1;
|
||||
private static final int MESSAGE_TYPE_INCOMING_MULTIMEDIA = 2;
|
||||
private static final int MESSAGE_TYPE_INCOMING_TEXT = 3;
|
||||
private static final int MESSAGE_TYPE_UPDATE = 4;
|
||||
private static final int MESSAGE_TYPE_HEADER = 5;
|
||||
private static final int MESSAGE_TYPE_FOOTER = 6;
|
||||
private static final int MESSAGE_TYPE_PLACEHOLDER = 7;
|
||||
|
||||
private static final long HEADER_ID = Long.MIN_VALUE;
|
||||
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
|
||||
@@ -136,9 +138,9 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
} else if (messageRecord.isUpdate()) {
|
||||
return MESSAGE_TYPE_UPDATE;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
return MESSAGE_TYPE_OUTGOING;
|
||||
return messageRecord.isMms() ? MESSAGE_TYPE_OUTGOING_MULTIMEDIA : MESSAGE_TYPE_OUTGOING_TEXT;
|
||||
} else {
|
||||
return MESSAGE_TYPE_INCOMING;
|
||||
return messageRecord.isMms() ? MESSAGE_TYPE_INCOMING_MULTIMEDIA : MESSAGE_TYPE_INCOMING_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +169,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case MESSAGE_TYPE_INCOMING:
|
||||
case MESSAGE_TYPE_OUTGOING:
|
||||
case MESSAGE_TYPE_INCOMING_TEXT:
|
||||
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_OUTGOING_TEXT:
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
@@ -189,7 +193,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
itemView.setEventListener(clickListener);
|
||||
|
||||
Log.d(TAG, "Inflate time: " + (System.currentTimeMillis() - start));
|
||||
Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType));
|
||||
return new ConversationViewHolder(itemView);
|
||||
case MESSAGE_TYPE_PLACEHOLDER:
|
||||
View v = new FrameLayout(parent.getContext());
|
||||
@@ -206,8 +210,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
switch (getItemViewType(position)) {
|
||||
case MESSAGE_TYPE_INCOMING:
|
||||
case MESSAGE_TYPE_OUTGOING:
|
||||
case MESSAGE_TYPE_INCOMING_TEXT:
|
||||
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_OUTGOING_TEXT:
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
||||
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
|
||||
@@ -358,8 +364,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
*/
|
||||
void pulseHighlightItem(int position) {
|
||||
if (position >= 0 && position < getItemCount()) {
|
||||
recordToPulseHighlight = getItem(position);
|
||||
notifyItemChanged(position);
|
||||
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
|
||||
|
||||
recordToPulseHighlight = getItem(correctedPosition);
|
||||
notifyItemChanged(correctedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,8 +430,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
*/
|
||||
@MainThread
|
||||
static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_MULTIMEDIA, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1);
|
||||
pool.setMaxRecycledViews(MESSAGE_TYPE_FOOTER, 1);
|
||||
@@ -462,12 +472,14 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return hasFooter() && position == (getItemCount() - 1);
|
||||
}
|
||||
|
||||
private @LayoutRes int getLayoutForViewType(int viewType) {
|
||||
private static @LayoutRes int getLayoutForViewType(int viewType) {
|
||||
switch (viewType) {
|
||||
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
|
||||
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
|
||||
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
|
||||
default: throw new IllegalArgumentException("Unknown type!");
|
||||
case MESSAGE_TYPE_OUTGOING_TEXT: return R.layout.conversation_item_sent_text_only;
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: return R.layout.conversation_item_sent_multimedia;
|
||||
case MESSAGE_TYPE_INCOMING_TEXT: return R.layout.conversation_item_received_text_only;
|
||||
case MESSAGE_TYPE_INCOMING_MULTIMEDIA: return R.layout.conversation_item_received_multimedia;
|
||||
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
|
||||
default: throw new IllegalArgumentException("Unknown type!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
@@ -50,7 +49,10 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
};
|
||||
|
||||
invalidator.observe(this::invalidate);
|
||||
invalidator.observe(() -> {
|
||||
invalidate();
|
||||
context.getContentResolver().unregisterContentObserver(contentObserver);
|
||||
});
|
||||
|
||||
context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadId), true, contentObserver);
|
||||
}
|
||||
@@ -59,36 +61,30 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<MessageRecord> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.requestedLoadSize);
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.requestedLoadSize);
|
||||
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()) {
|
||||
records.add(record);
|
||||
effectiveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInvalid()) {
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "[Initial Load] Invalidated before we could even query!");
|
||||
SizeFixResult result = ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.messages, params.requestedStartPosition, result.total);
|
||||
Util.runOnMain(dataUpdateCallback::onDataUpdated);
|
||||
}
|
||||
|
||||
int effectiveCount = records.size() + params.requestedStartPosition;
|
||||
int totalCount = db.getConversationCount(threadId);
|
||||
|
||||
if (effectiveCount > totalCount) {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Miscalculation! Records: %d, Start Position: %d, Total: %d. Adjusting total.",
|
||||
records.size(),
|
||||
params.requestedStartPosition,
|
||||
totalCount));
|
||||
totalCount = effectiveCount;
|
||||
}
|
||||
|
||||
records = ensureMultipleOfPageSize(records, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(records, params.requestedStartPosition, totalCount);
|
||||
Util.runOnMain(dataUpdateCallback::onDataUpdated);
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
@@ -99,15 +95,11 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.loadSize);
|
||||
|
||||
if (!isInvalid()) {
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
records.add(record);
|
||||
}
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
records.add(record);
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "[Update] Invalidated before we could even query!");
|
||||
}
|
||||
|
||||
callback.onResult(records);
|
||||
@@ -116,12 +108,33 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
private static @NonNull List<MessageRecord> ensureMultipleOfPageSize(@NonNull List<MessageRecord> records, int pageSize, int total) {
|
||||
if (records.size() != total && records.size() % pageSize != 0) {
|
||||
int overflow = records.size() % pageSize;
|
||||
return records.subList(0, records.size() - overflow);
|
||||
} else {
|
||||
return records;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,8 +167,8 @@ public class ConversationFragment extends Fragment {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2);
|
||||
}
|
||||
@@ -217,6 +217,7 @@ public class ConversationFragment extends Fragment {
|
||||
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
conversationViewModel.getMessages().observe(this, list -> {
|
||||
if (getListAdapter() != null) {
|
||||
Log.i(TAG, "submitList");
|
||||
getListAdapter().submitList(list);
|
||||
}
|
||||
});
|
||||
@@ -283,6 +284,7 @@ public class ConversationFragment extends Fragment {
|
||||
super.onResume();
|
||||
|
||||
if (list.getAdapter() != null) {
|
||||
Log.i(TAG, "onResume notifyDataSetChanged");
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
@@ -905,10 +907,17 @@ public class ConversationFragment extends Fragment {
|
||||
private void scrollToStartingPosition(int startingPosition) {
|
||||
list.post(() -> {
|
||||
list.getLayoutManager().scrollToPosition(startingPosition);
|
||||
getListAdapter().pulseHighlightItem(startingPosition);
|
||||
|
||||
if (shouldHighlightStartingPosition()) {
|
||||
getListAdapter().pulseHighlightItem(startingPosition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean shouldHighlightStartingPosition() {
|
||||
return requireActivity().getIntent().getBooleanExtra(ConversationActivity.HIGHLIGHT_STARTING_POSITION_EXTRA, false);
|
||||
}
|
||||
|
||||
private void scrollToLastSeenPosition(int lastSeenPosition) {
|
||||
if (lastSeenPosition > 0) {
|
||||
list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
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.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
liveRecipient = new MutableLiveData<>();
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, this::getGroupRecordForRecipient);
|
||||
groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, this::mapToGroupActiveState));
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
liveRecipient.setValue(recipient);
|
||||
}
|
||||
|
||||
LiveData<GroupActiveState> getGroupActiveState() {
|
||||
return groupActiveState;
|
||||
}
|
||||
|
||||
private GroupRecord getGroupRecordForRecipient(Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
return groupDatabase.getGroup(recipient.getId()).orNull();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return new GroupActiveState(record.isActive(), record.isV2Group());
|
||||
}
|
||||
|
||||
static final class GroupActiveState {
|
||||
private final boolean isActive;
|
||||
private final boolean isActiveV2;
|
||||
|
||||
public GroupActiveState(boolean isActive, boolean isV2) {
|
||||
this.isActive = isActive;
|
||||
this.isActiveV2 = isActive && isV2;
|
||||
}
|
||||
|
||||
public boolean isActiveGroup() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public boolean isActiveV2Group() {
|
||||
return isActiveV2;
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationGroupViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,6 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
@@ -150,20 +149,20 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
protected ViewGroup contactPhotoHolder;
|
||||
private QuoteView quoteView;
|
||||
private EmojiTextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
private ConversationItemFooter stickerFooter;
|
||||
private TextView groupSender;
|
||||
private TextView groupSenderProfileName;
|
||||
private View groupSenderHolder;
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ViewGroup container;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
@Nullable protected ViewGroup contactPhotoHolder;
|
||||
@Nullable private QuoteView quoteView;
|
||||
private EmojiTextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
private ConversationItemFooter stickerFooter;
|
||||
@Nullable private TextView groupSender;
|
||||
@Nullable private TextView groupSenderProfileName;
|
||||
@Nullable private View groupSenderHolder;
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ViewGroup container;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
@@ -285,6 +284,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
ConversationSwipeAnimationHelper.update(this, 0f, 1f);
|
||||
unbind();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@@ -311,6 +311,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
boolean needsMeasure = false;
|
||||
|
||||
if (hasQuote(messageRecord)) {
|
||||
if (quoteView == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
int quoteWidth = quoteView.getMeasuredWidth();
|
||||
int availableWidth = getAvailableMessageBubbleWidth(quoteView);
|
||||
|
||||
@@ -343,9 +346,10 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient modified) {
|
||||
setBubbleState(messageRecord);
|
||||
setContactPhoto(recipient.get());
|
||||
setGroupMessageStatus(messageRecord, recipient.get());
|
||||
setAudioViewTint(messageRecord, conversationRecipient.get());
|
||||
if (recipient.getId().equals(modified.getId())) {
|
||||
setContactPhoto(modified);
|
||||
setGroupMessageStatus(messageRecord, modified);
|
||||
}
|
||||
}
|
||||
|
||||
private int getAvailableMessageBubbleWidth(@NonNull View forView) {
|
||||
@@ -376,6 +380,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (recipient != null) {
|
||||
recipient.removeForeverObserver(this);
|
||||
}
|
||||
if (conversationRecipient != null) {
|
||||
conversationRecipient.removeForeverObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
public MessageRecord getMessageRecord() {
|
||||
@@ -403,19 +410,21 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
bodyBubble.setOutliner(shouldDrawBodyBubbleOutline(messageRecord) ? outliner : null);
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
setAudioViewTint(messageRecord, this.conversationRecipient.get());
|
||||
setAudioViewTint(messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAudioViewTint(MessageRecord messageRecord, Recipient recipient) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
if (DynamicTheme.isDarkTheme(context)) {
|
||||
audioViewStub.get().setTint(Color.WHITE, defaultBubbleColor);
|
||||
private void setAudioViewTint(MessageRecord messageRecord) {
|
||||
if (hasAudio(messageRecord)) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
if (DynamicTheme.isDarkTheme(context)) {
|
||||
audioViewStub.get().setTint(Color.WHITE);
|
||||
} else {
|
||||
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.core_grey_60));
|
||||
}
|
||||
} else {
|
||||
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.core_grey_60), defaultBubbleColor);
|
||||
audioViewStub.get().setTint(Color.WHITE);
|
||||
}
|
||||
} else {
|
||||
audioViewStub.get().setTint(Color.WHITE, recipient.getColor().toConversationColor(context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +595,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setSharedContactCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
footer.setVisibility(GONE);
|
||||
} else if (hasLinkPreview(messageRecord)) {
|
||||
linkPreviewStub.get().setVisibility(View.VISIBLE);
|
||||
@@ -613,13 +622,13 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
} else {
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true);
|
||||
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
linkPreviewStub.get().setOnClickListener(linkPreviewClickListener);
|
||||
@@ -642,7 +651,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasDocument(messageRecord)) {
|
||||
@@ -661,7 +670,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
documentViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) {
|
||||
@@ -683,7 +692,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
stickerStub.get().setOnClickListener(passthroughClickListener);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasThumbnail(messageRecord)) {
|
||||
@@ -713,7 +722,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else {
|
||||
@@ -726,7 +735,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
}
|
||||
@@ -870,6 +879,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
|
||||
if (quoteView == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
@@ -906,7 +918,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
ViewUtil.setTopMargin(mediaThumbnailStub.get(), readDimen(R.dimen.message_bubble_top_padding));
|
||||
}
|
||||
} else {
|
||||
quoteView.dismiss();
|
||||
if (quoteView != null) {
|
||||
quoteView.dismiss();
|
||||
}
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
ViewUtil.setTopMargin(mediaThumbnailStub.get(), 0);
|
||||
@@ -987,38 +1001,42 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
|
||||
if (groupThread && !messageRecord.isOutgoing()) {
|
||||
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
|
||||
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
this.groupSender.setText(recipient.getDisplayName(getContext()));
|
||||
this.groupSenderProfileName.setVisibility(View.GONE);
|
||||
groupSender.setText(recipient.getDisplayName(getContext()));
|
||||
groupSenderProfileName.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.groupSender.setText(recipient.toShortString(context));
|
||||
groupSender.setText(recipient.toShortString(context));
|
||||
|
||||
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
|
||||
this.groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
|
||||
this.groupSenderProfileName.setVisibility(View.VISIBLE);
|
||||
groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
|
||||
groupSenderProfileName.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.groupSenderProfileName.setText(null);
|
||||
this.groupSenderProfileName.setVisibility(View.GONE);
|
||||
groupSenderProfileName.setText(null);
|
||||
groupSenderProfileName.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) {
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord)) {
|
||||
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
|
||||
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
|
||||
} else if (hasSticker(messageRecord)) {
|
||||
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
|
||||
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
|
||||
} else {
|
||||
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
if (groupSender != null && groupSenderProfileName != null) {
|
||||
int stickerAuthorColor = ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color);
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
groupSenderProfileName.setTextColor(stickerAuthorColor);
|
||||
} else if (hasSticker(messageRecord)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
groupSenderProfileName.setTextColor(stickerAuthorColor);
|
||||
} else {
|
||||
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (isGroupThread && !current.isOutgoing()) {
|
||||
contactPhotoHolder.setVisibility(VISIBLE);
|
||||
@@ -1037,7 +1055,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
contactPhoto.setVisibility(GONE);
|
||||
}
|
||||
} else {
|
||||
groupSenderHolder.setVisibility(GONE);
|
||||
if (groupSenderHolder != null) {
|
||||
groupSenderHolder.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (contactPhotoHolder != null) {
|
||||
contactPhotoHolder.setVisibility(GONE);
|
||||
|
||||
@@ -120,8 +120,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
.map(e -> findViewById(e.viewId))
|
||||
.toArray(EmojiImageView[]::new);
|
||||
|
||||
customEmojiIndex = FeatureFlags.reactWithAnyEmoji() ? ReactionEmoji.values().length - 1
|
||||
: ReactionEmoji.values().length;
|
||||
customEmojiIndex = ReactionEmoji.values().length - 1;
|
||||
|
||||
distanceFromTouchDownPointToTopOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_top);
|
||||
distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
|
||||
|
||||
@@ -2,9 +2,6 @@ package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -12,6 +9,9 @@ import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -152,8 +151,6 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
}
|
||||
|
||||
private void setGroupRecipientTitle(Recipient recipient) {
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(getContext());
|
||||
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
this.title.setText(recipient.getDisplayName(getContext()));
|
||||
} else {
|
||||
@@ -161,8 +158,9 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
}
|
||||
|
||||
this.subtitle.setText(Stream.of(recipient.getParticipants())
|
||||
.filterNot(Recipient::isLocalNumber)
|
||||
.map(r -> r.toShortString(getContext()))
|
||||
.sorted((a, b) -> Boolean.compare(a.isLocalNumber(), b.isLocalNumber()))
|
||||
.map(r -> r.isLocalNumber() ? getResources().getString(R.string.ConversationTitleView_you)
|
||||
: r.getDisplayName(getContext()))
|
||||
.collect(Collectors.joining(", ")));
|
||||
|
||||
updateSubtitleVisibility();
|
||||
|
||||
@@ -87,6 +87,12 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
bind(messageRecord, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
unbind();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEventListener(@Nullable EventListener listener) {
|
||||
// No events to report yet
|
||||
|
||||
@@ -25,6 +25,9 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
@@ -37,8 +40,10 @@ import org.thoughtcrime.securesms.util.Conversions;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -59,9 +64,9 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
private final @Nullable ItemClickListener clickListener;
|
||||
private final @NonNull MessageDigest digest;
|
||||
|
||||
private final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
private final Map<Long, ThreadRecord> batchSet = Collections.synchronizedMap(new HashMap<>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
|
||||
@@ -143,7 +148,7 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet, batchMode);
|
||||
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet.keySet(), batchMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,16 +174,20 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
return threadDatabase.readerFor(cursor).getCurrent();
|
||||
}
|
||||
|
||||
void toggleThreadInBatchSet(long threadId) {
|
||||
if (batchSet.contains(threadId)) {
|
||||
batchSet.remove(threadId);
|
||||
} else if (threadId != -1) {
|
||||
batchSet.add(threadId);
|
||||
void toggleThreadInBatchSet(@NonNull ThreadRecord thread) {
|
||||
if (batchSet.containsKey(thread.getThreadId())) {
|
||||
batchSet.remove(thread.getThreadId());
|
||||
} else if (thread.getThreadId() != -1) {
|
||||
batchSet.put(thread.getThreadId(), thread);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Long> getBatchSelections() {
|
||||
return batchSet;
|
||||
@NonNull Set<Long> getBatchSelectionIds() {
|
||||
return batchSet.keySet();
|
||||
}
|
||||
|
||||
@NonNull Set<ThreadRecord> getBatchSelection() {
|
||||
return new HashSet<>(batchSet.values());
|
||||
}
|
||||
|
||||
void initializeBatchMode(boolean toggle) {
|
||||
@@ -193,8 +202,10 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
|
||||
void selectAllThreads() {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
long threadId = getThreadRecord(getCursorAtPositionOrThrow(i)).getThreadId();
|
||||
if (threadId != -1) batchSet.add(threadId);
|
||||
ThreadRecord record = getThreadRecord(getCursorAtPositionOrThrow(i));
|
||||
if (record.getThreadId() != -1) {
|
||||
batchSet.put(record.getThreadId(), record);
|
||||
}
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
@@ -90,7 +91,6 @@ 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.conversation.ConversationFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
@@ -103,7 +103,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockV1Dialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
@@ -113,13 +112,12 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -128,6 +126,7 @@ 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;
|
||||
@@ -240,8 +239,6 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
RatingManager.showRatingDialogIfNecessary(requireContext());
|
||||
|
||||
RegistrationLockV1Dialog.showReminderIfNecessary(this);
|
||||
|
||||
TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
|
||||
}
|
||||
|
||||
@@ -356,19 +353,24 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
|
||||
threadRecord.getThreadId(),
|
||||
threadRecord.getDistributionType(),
|
||||
-1);
|
||||
threadRecord.getUnreadCount(),
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactClicked(@NonNull Recipient contact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
|
||||
}, threadId -> {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
|
||||
int unreadCount = DatabaseFactory.getMmsSmsDatabase(getContext()).getUnreadCount(threadId);
|
||||
|
||||
return new Pair<>(threadId, unreadCount);
|
||||
}, pair -> {
|
||||
hideKeyboard();
|
||||
getNavigator().goToConversation(contact.getId(),
|
||||
threadId,
|
||||
pair.first(),
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
-1);
|
||||
pair.second(),
|
||||
false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,7 +384,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
getNavigator().goToConversation(message.conversationRecipient.getId(),
|
||||
message.threadId,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
startingPosition);
|
||||
startingPosition,
|
||||
true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -591,11 +594,46 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setAllThreadsRead();
|
||||
|
||||
MessageNotifier.updateNotification(context);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMarkSelectedAsRead() {
|
||||
Context context = requireContext();
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(selectedConversations, false);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
return null;
|
||||
}, none -> {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMarkSelectedAsUnread() {
|
||||
Context context = requireContext();
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
DatabaseFactory.getThreadDatabase(context).setForcedUnread(selectedConversations);
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
return null;
|
||||
}, none -> {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleInvite() {
|
||||
getNavigator().goToInvite();
|
||||
}
|
||||
@@ -606,7 +644,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleArchiveAllSelected() {
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelections());
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
int count = selectedConversations.size();
|
||||
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
|
||||
|
||||
@@ -645,7 +683,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDeleteAllSelected() {
|
||||
int conversationsCount = defaultAdapter.getBatchSelections().size();
|
||||
int conversationsCount = defaultAdapter.getBatchSelectionIds().size();
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
||||
alert.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
alert.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
|
||||
@@ -655,7 +693,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
alert.setCancelable(true);
|
||||
|
||||
alert.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
final Set<Long> selectedConversations = defaultAdapter.getBatchSelections();
|
||||
final Set<Long> selectedConversations = defaultAdapter.getBatchSelectionIds();
|
||||
|
||||
if (!selectedConversations.isEmpty()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@@ -672,7 +710,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations);
|
||||
MessageNotifier.updateNotification(getActivity());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -694,11 +732,11 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
private void handleSelectAllThreads() {
|
||||
defaultAdapter.selectAllThreads();
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size()));
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
|
||||
}
|
||||
|
||||
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) {
|
||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, int unreadCount) {
|
||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, unreadCount, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -732,15 +770,16 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
@Override
|
||||
public void onItemClick(ConversationListItem item) {
|
||||
if (actionMode == null) {
|
||||
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType());
|
||||
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType(), item.getUnreadCount());
|
||||
} else {
|
||||
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
|
||||
adapter.toggleThreadInBatchSet(item.getThreadId());
|
||||
adapter.toggleThreadInBatchSet(item.getThread());
|
||||
|
||||
if (adapter.getBatchSelections().size() == 0) {
|
||||
if (adapter.getBatchSelectionIds().size() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size()));
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
|
||||
setCorrectMenuVisibility(actionMode.getMenu());
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged();
|
||||
@@ -752,7 +791,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleThreadInBatchSet(item.getThreadId());
|
||||
defaultAdapter.toggleThreadInBatchSet(item.getThread());
|
||||
defaultAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -784,15 +823,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
setCorrectMenuVisibility(menu);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_select_all: handleSelectAllThreads(); return true;
|
||||
case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
|
||||
case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
|
||||
case R.id.menu_select_all: handleSelectAllThreads(); return true;
|
||||
case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
|
||||
case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
|
||||
case R.id.menu_mark_as_read: handleMarkSelectedAsRead(); return true;
|
||||
case R.id.menu_mark_as_unread: handleMarkSelectedAsUnread(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -833,6 +875,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
closeSearchIfOpen();
|
||||
}
|
||||
|
||||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(thread -> !thread.isRead());
|
||||
|
||||
if (hasUnread) {
|
||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
||||
menu.findItem(R.id.menu_mark_as_read).setVisible(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(true);
|
||||
menu.findItem(R.id.menu_mark_as_read).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected @IdRes int getToolbarRes() {
|
||||
return R.id.toolbar;
|
||||
}
|
||||
@@ -873,7 +927,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
if (unreadCount > 0) {
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(getActivity()).setRead(threadId, false);
|
||||
MessageNotifier.updateNotification(getActivity());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
MarkReadReceiver.process(getActivity(), messageIds);
|
||||
}
|
||||
}
|
||||
@@ -884,7 +938,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
if (unreadCount > 0) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).incrementUnread(threadId, unreadCount);
|
||||
MessageNotifier.updateNotification(getActivity());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.graphics.Typeface;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
@@ -41,6 +42,9 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
@@ -48,8 +52,11 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -84,6 +91,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
private TextView unreadIndicator;
|
||||
private long lastSeen;
|
||||
private ThreadRecord thread;
|
||||
private boolean batchMode;
|
||||
|
||||
private int unreadCount;
|
||||
private AvatarImageView contactPhotoImage;
|
||||
@@ -93,7 +101,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
private final RecipientForeverObserver groupAddedByObserver = adder -> {
|
||||
if (isAttachedToWindow() && subjectView != null && thread != null) {
|
||||
subjectView.setText(thread.getDisplayBody(getContext()));
|
||||
subjectView.setText(getThreadDisplayBody(getContext(), thread));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +170,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
|
||||
} else {
|
||||
this.fromView.setText(recipient.get(), unreadCount == 0);
|
||||
this.fromView.setText(recipient.get(), thread.isRead());
|
||||
}
|
||||
|
||||
if (typingThreads.contains(threadId)) {
|
||||
@@ -175,24 +183,24 @@ public class ConversationListItem extends RelativeLayout
|
||||
this.typingView.stopAnimation();
|
||||
|
||||
this.subjectView.setVisibility(VISIBLE);
|
||||
this.subjectView.setText(getTrimmedSnippet(thread.getDisplayBody(getContext())));
|
||||
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
|
||||
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
|
||||
groupAddedBy.observeForever(groupAddedByObserver);
|
||||
}
|
||||
|
||||
this.subjectView.setTypeface(unreadCount == 0 ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
this.subjectView.setTextColor(unreadCount == 0 ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
|
||||
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);
|
||||
dateView.setTypeface(unreadCount == 0 ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
dateView.setTextColor(unreadCount == 0 ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_date_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
|
||||
dateView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
dateView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_date_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
|
||||
}
|
||||
|
||||
if (thread.isArchived()) {
|
||||
@@ -203,10 +211,10 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
setStatusIcons(thread);
|
||||
setThumbnailSnippet(thread);
|
||||
setBatchState(batchMode);
|
||||
setBatchMode(batchMode);
|
||||
setRippleColor(recipient.get());
|
||||
setUnreadIndicator(thread);
|
||||
this.contactPhotoImage.setAvatar(glideRequests, recipient.get(), true);
|
||||
this.contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
}
|
||||
|
||||
public void bind(@NonNull Recipient contact,
|
||||
@@ -233,9 +241,9 @@ public class ConversationListItem extends RelativeLayout
|
||||
alertView.setNone();
|
||||
thumbnailView.setVisibility(GONE);
|
||||
|
||||
setBatchState(false);
|
||||
setBatchMode(false);
|
||||
setRippleColor(contact);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), true);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
}
|
||||
|
||||
public void bind(@NonNull MessageResult messageResult,
|
||||
@@ -261,9 +269,9 @@ public class ConversationListItem extends RelativeLayout
|
||||
alertView.setNone();
|
||||
thumbnailView.setVisibility(GONE);
|
||||
|
||||
setBatchState(false);
|
||||
setBatchMode(false);
|
||||
setRippleColor(recipient.get());
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), true);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -271,7 +279,9 @@ public class ConversationListItem extends RelativeLayout
|
||||
if (this.recipient != null) {
|
||||
this.recipient.removeForeverObserver(this);
|
||||
this.recipient = null;
|
||||
contactPhotoImage.setAvatar(glideRequests, null, true);
|
||||
|
||||
setBatchMode(false);
|
||||
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
||||
}
|
||||
|
||||
if (this.groupAddedBy != null) {
|
||||
@@ -280,8 +290,9 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void setBatchState(boolean batch) {
|
||||
setSelected(batch && selectedThreads.contains(threadId));
|
||||
private void setBatchMode(boolean batchMode) {
|
||||
this.batchMode = batchMode;
|
||||
setSelected(batchMode && selectedThreads.contains(thread.getThreadId()));
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
@@ -292,6 +303,10 @@ public class ConversationListItem extends RelativeLayout
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public @NonNull ThreadRecord getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public int getUnreadCount() {
|
||||
return unreadCount;
|
||||
}
|
||||
@@ -304,7 +319,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
|
||||
private static @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
|
||||
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet
|
||||
: snippet.subSequence(0, MAX_SNIPPET_LENGTH);
|
||||
}
|
||||
@@ -316,9 +331,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer .getLayoutParams();
|
||||
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.thumbnail);
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
subjectParams.addRule(RelativeLayout.START_OF, R.id.thumbnail);
|
||||
}
|
||||
subjectParams.addRule(RelativeLayout.START_OF, R.id.thumbnail);
|
||||
this.subjectContainer.setLayoutParams(subjectParams);
|
||||
this.post(new ThumbnailPositioner(thumbnailView, archivedView, deliveryStatusIndicator, dateView));
|
||||
} else {
|
||||
@@ -326,9 +339,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer.getLayoutParams();
|
||||
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
subjectParams.addRule(RelativeLayout.START_OF, R.id.status);
|
||||
}
|
||||
subjectParams.addRule(RelativeLayout.START_OF, R.id.status);
|
||||
this.subjectContainer.setLayoutParams(subjectParams);
|
||||
}
|
||||
}
|
||||
@@ -361,22 +372,109 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
private void setUnreadIndicator(ThreadRecord thread) {
|
||||
if (thread.isOutgoing() || thread.getUnreadCount() == 0) {
|
||||
if ((thread.isOutgoing() && !thread.isForcedUnread()) || thread.isRead()) {
|
||||
unreadIndicator.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
unreadIndicator.setText(String.valueOf(unreadCount));
|
||||
unreadIndicator.setText(unreadCount > 0 ? String.valueOf(unreadCount) : " ");
|
||||
unreadIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
fromView.setText(recipient, unreadCount == 0);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, true);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode);
|
||||
setRippleColor(recipient);
|
||||
}
|
||||
|
||||
|
||||
private static SpannableString getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
return emphasisAdded(context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
Recipient.live(thread.getGroupAddedBy()).get().getDisplayName(context)));
|
||||
} else if (!thread.isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
|
||||
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
|
||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
||||
return emphasisAdded(draftText + " " + thread.getBody(), 0, draftText.length());
|
||||
} else if (SmsDatabase.Types.isOutgoingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called_you));
|
||||
} 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)));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
int seconds = (int)(thread.getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
||||
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
|
||||
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)));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
||||
} else if (SmsDatabase.Types.isUnsupportedMessageType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_could_not_be_processed));
|
||||
} else {
|
||||
ThreadDatabase.Extra extra = thread.getExtra();
|
||||
if (extra != null && extra.isViewOnce()) {
|
||||
return new SpannableString(emphasisAdded(getViewOnceDescription(context, thread.getContentType())));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_this_message_was_deleted)));
|
||||
} else {
|
||||
return new SpannableString(Util.emptyIfNull(thread.getBody()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start,
|
||||
end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
private static String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
|
||||
if (MediaUtil.isViewOnceType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_view_once_media);
|
||||
} else if (MediaUtil.isVideoType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_view_once_video);
|
||||
} else {
|
||||
return context.getString(R.string.ThreadRecord_view_once_photo);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThumbnailPositioner implements Runnable {
|
||||
|
||||
private final View thumbnailView;
|
||||
@@ -399,14 +497,10 @@ public class ConversationListItem extends RelativeLayout
|
||||
(archivedView.getWidth() + deliveryStatusView.getWidth()) > dateView.getWidth())
|
||||
{
|
||||
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.status);
|
||||
}
|
||||
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.status);
|
||||
} else {
|
||||
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.date);
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.date);
|
||||
}
|
||||
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.date);
|
||||
}
|
||||
|
||||
thumbnailView.setLayoutParams(thumbnailParams);
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
@@ -44,12 +45,14 @@ import org.json.JSONException;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
@@ -120,7 +123,7 @@ public class AttachmentDatabase extends Database {
|
||||
static final String HEIGHT = "height";
|
||||
static final String CAPTION = "caption";
|
||||
private static final String DATA_HASH = "data_hash";
|
||||
static final String BLUR_HASH = "blur_hash";
|
||||
static final String VISUAL_HASH = "blur_hash";
|
||||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
static final String UPLOAD_TIMESTAMP = "upload_timestamp";
|
||||
@@ -145,7 +148,7 @@ public class AttachmentDatabase extends Database {
|
||||
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
|
||||
FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM,
|
||||
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, BLUR_HASH,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, VISUAL_HASH,
|
||||
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
|
||||
UPLOAD_TIMESTAMP };
|
||||
|
||||
@@ -182,7 +185,7 @@ public class AttachmentDatabase extends Database {
|
||||
STICKER_PACK_KEY + " DEFAULT NULL, " +
|
||||
STICKER_ID + " INTEGER DEFAULT -1, " +
|
||||
DATA_HASH + " TEXT DEFAULT NULL, " +
|
||||
BLUR_HASH + " TEXT DEFAULT NULL, " +
|
||||
VISUAL_HASH + " TEXT DEFAULT NULL, " +
|
||||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " +
|
||||
TRANSFER_FILE + " TEXT DEFAULT NULL, " +
|
||||
DISPLAY_ORDER + " INTEGER DEFAULT 0, " +
|
||||
@@ -417,7 +420,7 @@ public class AttachmentDatabase extends Database {
|
||||
values.put(WIDTH, 0);
|
||||
values.put(HEIGHT, 0);
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(BLUR_HASH, (String) null);
|
||||
values.put(VISUAL_HASH, (String) null);
|
||||
values.put(CONTENT_TYPE, MediaUtil.VIEW_ONCE);
|
||||
|
||||
database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""});
|
||||
@@ -530,8 +533,9 @@ public class AttachmentDatabase extends Database {
|
||||
values.put(DATA_HASH, dataInfo.hash);
|
||||
}
|
||||
|
||||
if (placeholder != null && placeholder.getBlurHash() != null) {
|
||||
values.put(BLUR_HASH, placeholder.getBlurHash().getHash());
|
||||
String visualHashString = getVisualHashStringOrNull(placeholder);
|
||||
if (visualHashString != null) {
|
||||
values.put(VISUAL_HASH, visualHashString);
|
||||
}
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
@@ -555,9 +559,11 @@ public class AttachmentDatabase extends Database {
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
|
||||
}
|
||||
|
||||
private static @Nullable String getBlurHashStringOrNull(@Nullable BlurHash blurHash) {
|
||||
if (blurHash == null) return null;
|
||||
return blurHash.getHash();
|
||||
private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
|
||||
if (attachment == null) return null;
|
||||
else if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
|
||||
else if (attachment.getAudioHash() != null) return attachment.getAudioHash().getHash();
|
||||
else return null;
|
||||
}
|
||||
|
||||
public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull AttachmentId destinationId)
|
||||
@@ -594,7 +600,7 @@ public class AttachmentDatabase extends Database {
|
||||
contentValues.put(WIDTH, sourceAttachment.getWidth());
|
||||
contentValues.put(HEIGHT, sourceAttachment.getHeight());
|
||||
contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType());
|
||||
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(sourceAttachment.getBlurHash()));
|
||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(sourceAttachment));
|
||||
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings());
|
||||
}
|
||||
@@ -638,7 +644,7 @@ public class AttachmentDatabase extends Database {
|
||||
values.put(NAME, attachment.getRelay());
|
||||
values.put(SIZE, attachment.getSize());
|
||||
values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
|
||||
values.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash()));
|
||||
values.put(VISUAL_HASH, getVisualHashStringOrNull(attachment));
|
||||
values.put(UPLOAD_TIMESTAMP, uploadTimestamp);
|
||||
|
||||
if (dataInfo != null && dataInfo.hash != null) {
|
||||
@@ -1099,11 +1105,12 @@ public class AttachmentDatabase extends Database {
|
||||
JsonUtils.SaneJSONObject object = new JsonUtils.SaneJSONObject(array.getJSONObject(i));
|
||||
|
||||
if (!object.isNull(ROW_ID)) {
|
||||
String contentType = object.getString(CONTENT_TYPE);
|
||||
result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)),
|
||||
object.getLong(MMS_ID),
|
||||
!TextUtils.isEmpty(object.getString(DATA)),
|
||||
!TextUtils.isEmpty(object.getString(THUMBNAIL)),
|
||||
object.getString(CONTENT_TYPE),
|
||||
contentType,
|
||||
object.getInt(TRANSFER_STATE),
|
||||
object.getLong(SIZE),
|
||||
object.getString(FILE_NAME),
|
||||
@@ -1123,7 +1130,8 @@ public class AttachmentDatabase extends Database {
|
||||
object.getString(STICKER_PACK_KEY),
|
||||
object.getInt(STICKER_ID))
|
||||
: null,
|
||||
BlurHash.parseOrNull(object.getString(BLUR_HASH)),
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(object.getString(VISUAL_HASH)),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(object.getString(VISUAL_HASH)) : null,
|
||||
TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES)),
|
||||
object.getInt(DISPLAY_ORDER),
|
||||
object.getLong(UPLOAD_TIMESTAMP)));
|
||||
@@ -1132,12 +1140,13 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
return result;
|
||||
} else {
|
||||
String contentType = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE));
|
||||
return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
|
||||
contentType,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
|
||||
@@ -1157,7 +1166,8 @@ public class AttachmentDatabase extends Database {
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)))
|
||||
: null,
|
||||
BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(BLUR_HASH))),
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null,
|
||||
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP))));
|
||||
@@ -1167,7 +1177,6 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote)
|
||||
throws MmsException
|
||||
{
|
||||
@@ -1219,11 +1228,11 @@ public class AttachmentDatabase extends Database {
|
||||
contentValues.put(CAPTION, attachment.getCaption());
|
||||
contentValues.put(UPLOAD_TIMESTAMP, useTemplateUpload ? template.getUploadTimestamp() : attachment.getUploadTimestamp());
|
||||
if (attachment.getTransformProperties().isVideoEdited()) {
|
||||
contentValues.putNull(BLUR_HASH);
|
||||
contentValues.putNull(VISUAL_HASH);
|
||||
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
|
||||
} else {
|
||||
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(template.getBlurHash()));
|
||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
|
||||
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = STANDARD_THUMB_TIME;
|
||||
}
|
||||
@@ -1330,6 +1339,21 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) {
|
||||
Log.i(TAG, "updating part audio wave form for #" + attachmentId);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
if (audioWaveForm != null) {
|
||||
values.put(VISUAL_HASH, new AudioHash(audioWaveForm).getHash());
|
||||
} else {
|
||||
values.putNull(VISUAL_HASH);
|
||||
}
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class ThumbnailFetchCallable implements Callable<InputStream> {
|
||||
|
||||
@@ -31,6 +31,8 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView.
|
||||
*/
|
||||
@@ -173,6 +175,16 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
||||
|
||||
public abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
|
||||
@Override
|
||||
public final void onBindViewHolder(@NonNull ViewHolder viewHolder, int position, @NonNull List<Object> payloads) {
|
||||
if (arePayloadsValid(payloads) && !isHeaderPosition(position) && !isFooterPosition(position)) {
|
||||
if (isFastAccessPosition(position)) onBindFastAccessItemViewHolder((VH)viewHolder, position, payloads);
|
||||
else onBindItemViewHolder((VH)viewHolder, getCursorAtPositionOrThrow(position), payloads);
|
||||
} else {
|
||||
onBindViewHolder(viewHolder, position);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
@@ -188,8 +200,17 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
||||
|
||||
public abstract void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor);
|
||||
|
||||
protected void onBindFastAccessItemViewHolder(VH viewHolder, int position) {
|
||||
protected boolean arePayloadsValid(@NonNull List<Object> payloads) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor, @NonNull List<Object> payloads) {
|
||||
}
|
||||
|
||||
protected void onBindFastAccessItemViewHolder(VH viewHolder, int position) {
|
||||
}
|
||||
|
||||
protected void onBindFastAccessItemViewHolder(VH viewHolder, int position, @NonNull List<Object> payloads) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -41,6 +40,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GroupDatabase extends Database {
|
||||
@@ -64,7 +64,7 @@ public final class GroupDatabase extends Database {
|
||||
|
||||
/* V2 Group columns */
|
||||
/** 32 bytes serialized {@link GroupMasterKey} */
|
||||
private static final String V2_MASTER_KEY = "master_key";
|
||||
public static final String V2_MASTER_KEY = "master_key";
|
||||
/** Increments with every change to the group */
|
||||
private static final String V2_REVISION = "revision";
|
||||
/** Serialized {@link DecryptedGroup} protobuf */
|
||||
@@ -252,7 +252,7 @@ public final class GroupDatabase extends Database {
|
||||
@WorkerThread
|
||||
public @NonNull List<Recipient> getGroupMembers(@NonNull GroupId groupId, @NonNull MemberSet memberSet) {
|
||||
if (groupId.isV2()) {
|
||||
return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMembers(context, memberSet))
|
||||
return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMemberRecipients(memberSet))
|
||||
.or(Collections.emptyList());
|
||||
} else {
|
||||
List<RecipientId> currentMembers = getCurrentMembers(groupId);
|
||||
@@ -336,9 +336,9 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
groupId.requireV2();
|
||||
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||
contentValues.put(V2_REVISION, groupState.getVersion());
|
||||
contentValues.put(V2_REVISION, groupState.getRevision());
|
||||
contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray());
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(context, groupState));
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(groupState));
|
||||
} else {
|
||||
if (groupId.isV2()) {
|
||||
throw new AssertionError("V2 group id but no master key");
|
||||
@@ -391,11 +391,14 @@ public final class GroupDatabase extends Database {
|
||||
RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
|
||||
String title = decryptedGroup.getTitle();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
UUID uuid = Recipient.self().getUuid().get();
|
||||
|
||||
contentValues.put(TITLE, title);
|
||||
contentValues.put(V2_REVISION, decryptedGroup.getVersion());
|
||||
contentValues.put(V2_REVISION, decryptedGroup.getRevision());
|
||||
contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray());
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(context, decryptedGroup));
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(decryptedGroup));
|
||||
contentValues.put(ACTIVE, DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), uuid).isPresent() ||
|
||||
DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), uuid).isPresent() ? 1 : 0);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
|
||||
GROUP_ID + " = ?",
|
||||
@@ -517,13 +520,16 @@ public final class GroupDatabase extends Database {
|
||||
return getGroup(groupId).transform(g -> g.isPendingMember(recipient)).or(false);
|
||||
}
|
||||
|
||||
private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) {
|
||||
private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
|
||||
List<RecipientId> groupMembers = new ArrayList<>(decryptedGroup.getMembersCount());
|
||||
|
||||
for (DecryptedMember member : decryptedGroup.getMembersList()) {
|
||||
Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.fromByteString(member.getUuid()), null));
|
||||
|
||||
groupMembers.add(recipient.getId());
|
||||
UUID uuid = UuidUtil.fromByteString(member.getUuid());
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
Log.w(TAG, "Seen unknown UUID in members list");
|
||||
} else {
|
||||
groupMembers.add(RecipientId.from(uuid, null));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(groupMembers);
|
||||
@@ -779,25 +785,41 @@ public final class GroupDatabase extends Database {
|
||||
.or(false);
|
||||
}
|
||||
|
||||
public List<Recipient> getMembers(@NonNull Context context, @NonNull MemberSet memberSet) {
|
||||
boolean includeSelf = memberSet.includeSelf;
|
||||
DecryptedGroup groupV2 = getDecryptedGroup();
|
||||
UUID selfUuid = Recipient.self().getUuid().get();
|
||||
List<Recipient> recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount());
|
||||
public List<Recipient> getMemberRecipients(@NonNull MemberSet memberSet) {
|
||||
return Stream.of(getMemberRecipientIds(memberSet))
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<RecipientId> getMemberRecipientIds(@NonNull MemberSet memberSet) {
|
||||
boolean includeSelf = memberSet.includeSelf;
|
||||
DecryptedGroup groupV2 = getDecryptedGroup();
|
||||
UUID selfUuid = Recipient.self().getUuid().get();
|
||||
List<RecipientId> recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount());
|
||||
int unknownMembers = 0;
|
||||
int unknownPending = 0;
|
||||
|
||||
for (UUID uuid : DecryptedGroupUtil.toUuidList(groupV2.getMembersList())) {
|
||||
if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(Recipient.externalPush(context, uuid, null));
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
unknownMembers++;
|
||||
} else if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(RecipientId.from(uuid, null));
|
||||
}
|
||||
}
|
||||
if (memberSet.includePending) {
|
||||
for (UUID uuid : DecryptedGroupUtil.pendingToUuidList(groupV2.getPendingMembersList())) {
|
||||
if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(Recipient.externalPush(context, uuid, null));
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
unknownPending++;
|
||||
} else if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(RecipientId.from(uuid, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((unknownMembers + unknownPending) > 0) {
|
||||
Log.w(TAG, String.format(Locale.US, "Group contains %d + %d unknown pending and full members", unknownPending, unknownMembers));
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
@@ -67,14 +68,24 @@ public class GroupReceiptDatabase extends Database {
|
||||
new String[] {String.valueOf(mmsId), recipientId.serialize(), String.valueOf(status)});
|
||||
}
|
||||
|
||||
public void setUnidentified(RecipientId recipientId, long mmsId, boolean unidentified) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(UNIDENTIFIED, unidentified ? 1 : 0);
|
||||
public void setUnidentified(Collection<Pair<RecipientId, Boolean>> results, long mmsId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.update(TABLE_NAME, values, MMS_ID + " = ? AND " + RECIPIENT_ID + " = ?",
|
||||
new String[] {String.valueOf(mmsId), recipientId.serialize()});
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = MMS_ID + " = ? AND " + RECIPIENT_ID + " = ?";
|
||||
|
||||
for (Pair<RecipientId, Boolean> result : results) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(UNIDENTIFIED, result.second() ? 1 : 0);
|
||||
|
||||
db.update(TABLE_NAME, values, query, new String[]{ String.valueOf(mmsId), result.first().serialize()});
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<GroupReceiptInfo> getGroupReceiptInfo(long mmsId) {
|
||||
|
||||
@@ -146,6 +146,7 @@ public class IdentityDatabase extends Database {
|
||||
}
|
||||
|
||||
public void updateIdentityAfterSync(@NonNull RecipientId id, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
boolean hadEntry = getIdentity(id).isPresent();
|
||||
boolean keyMatches = hasMatchingKey(id, identityKey);
|
||||
boolean statusMatches = keyMatches && hasMatchingStatus(id, identityKey, verifiedStatus);
|
||||
|
||||
@@ -155,7 +156,7 @@ public class IdentityDatabase extends Database {
|
||||
if (record.isPresent()) EventBus.getDefault().post(record.get());
|
||||
}
|
||||
|
||||
if (!keyMatches) {
|
||||
if (hadEntry && !keyMatches) {
|
||||
IdentityUtil.markIdentityUpdate(context, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class MediaDatabase extends Database {
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", "
|
||||
|
||||
@@ -218,7 +218,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +
|
||||
@@ -1083,9 +1083,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
if (message.isGroup()) {
|
||||
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (OutgoingGroupUpdateMessage) message;
|
||||
if (outgoingGroupUpdateMessage.isV2Group()) {
|
||||
MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupUpdateMessage.requireGroupV2Properties();
|
||||
type |= Types.GROUP_V2_BIT;
|
||||
if (groupV2Properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
|
||||
type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
|
||||
} else {
|
||||
MessageGroupContext.GroupV1Properties properties = outgoingGroupUpdateMessage.requireGroupV1Properties();
|
||||
if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
|
||||
@@ -1129,20 +1127,15 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
if (message.getRecipient().isGroup()) {
|
||||
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (message instanceof OutgoingGroupUpdateMessage) ? (OutgoingGroupUpdateMessage) message : null;
|
||||
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Set<RecipientId> members = new HashSet<>();
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
Set<RecipientId> members = new HashSet<>();
|
||||
|
||||
if (outgoingGroupUpdateMessage != null && outgoingGroupUpdateMessage.isV2Group()) {
|
||||
MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupUpdateMessage.requireGroupV2Properties();
|
||||
members.addAll(Stream.of(groupV2Properties.getActiveMembers()).map(recipientDatabase::getOrInsertFromUuid).toList());
|
||||
if (groupV2Properties.isUpdate()) {
|
||||
members.addAll(Stream.concat(Stream.of(groupV2Properties.getPendingMembers()),
|
||||
Stream.of(groupV2Properties.getRemovedMembers()))
|
||||
.distinct()
|
||||
.map(recipientDatabase::getOrInsertFromUuid)
|
||||
.toList());
|
||||
}
|
||||
members.addAll(Stream.of(groupV2Properties.getAllActivePendingAndRemovedMembers())
|
||||
.distinct()
|
||||
.map(uuid -> RecipientId.from(uuid, null))
|
||||
.toList());
|
||||
members.remove(Recipient.self().getId());
|
||||
} else {
|
||||
members.addAll(Stream.of(DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)).map(Recipient::getId).toList());
|
||||
|
||||
@@ -28,7 +28,6 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -387,7 +386,7 @@ public class MmsSmsDatabase extends Database {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +
|
||||
|
||||
@@ -50,7 +50,7 @@ public class PushDatabase extends Database {
|
||||
Optional<Long> messageId = find(envelope);
|
||||
|
||||
if (messageId.isPresent()) {
|
||||
return messageId.get();
|
||||
return -1;
|
||||
} else {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TYPE, envelope.getType());
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
@@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.jobs.WakeGroupV2Job;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
@@ -32,6 +35,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -43,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
@@ -124,27 +129,26 @@ public class RecipientDatabase extends Database {
|
||||
STORAGE_SERVICE_ID, DIRTY
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList().toArray(new String[0]);
|
||||
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
|
||||
new String[] { TABLE_NAME + "." + ID },
|
||||
RECIPIENT_PROJECTION,
|
||||
TYPED_RECIPIENT_PROJECTION,
|
||||
new String[] {
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
|
||||
});
|
||||
|
||||
|
||||
public static final String[] CREATE_INDEXS = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
|
||||
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");",
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList();
|
||||
|
||||
public enum VibrateState {
|
||||
DEFAULT(0), ENABLED(1), DISABLED(2);
|
||||
|
||||
@@ -240,7 +244,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public enum GroupType {
|
||||
NONE(0), MMS(1), SIGNAL_V1(2);
|
||||
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3);
|
||||
|
||||
private final int id;
|
||||
|
||||
@@ -365,7 +369,11 @@ public class RecipientDatabase extends Database {
|
||||
if (groupId.isMms()) {
|
||||
values.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
if (groupId.isV2()) {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
}
|
||||
values.put(DIRTY, DirtyState.INSERT.getId());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
@@ -422,29 +430,46 @@ public class RecipientDatabase extends Database {
|
||||
return DirtyState.CLEAN;
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) {
|
||||
String query = TABLE_NAME + "." + ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
|
||||
List<RecipientSettings> recipientSettingsForSync = getRecipientSettingsForSync(query, args);
|
||||
|
||||
if (recipientSettingsForSync.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recipientSettingsForSync.size() > 1) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
return recipientSettingsForSync.get(0);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) {
|
||||
List<RecipientSettings> result = getRecipientSettings(STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
|
||||
List<RecipientSettings> result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
|
||||
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
@@ -480,7 +505,9 @@ public class RecipientDatabase extends Database {
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates)
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates,
|
||||
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV2Record>> groupV2Updates)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
@@ -491,7 +518,7 @@ public class RecipientDatabase extends Database {
|
||||
try {
|
||||
|
||||
for (SignalContactRecord insert : contactInserts) {
|
||||
ContentValues values = getValuesForStorageContact(insert);
|
||||
ContentValues values = validateContactValuesForInsert(getValuesForStorageContact(insert));
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
|
||||
if (id < 0) {
|
||||
@@ -512,7 +539,6 @@ public class RecipientDatabase extends Database {
|
||||
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
|
||||
}
|
||||
@@ -586,6 +612,32 @@ public class RecipientDatabase extends Database {
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (SignalGroupV2Record insert : groupV2Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert));
|
||||
|
||||
GroupId.V2 groupId = GroupId.v2(insert.getMasterKey());
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new WakeGroupV2Job(insert.getMasterKey()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (RecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV2(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(update.getOld().getMasterKey()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@@ -596,13 +648,18 @@ public class RecipientDatabase extends Database {
|
||||
public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
ProfileName profileName = ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull());
|
||||
ContentValues values = new ContentValues();
|
||||
ProfileName profileName = ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull());
|
||||
String profileKey = update.getProfileKey().or(Optional.fromNullable(Recipient.self().getProfileKey())).transform(Base64::encodeBytes).orNull();
|
||||
|
||||
if (!update.getProfileKey().isPresent()) {
|
||||
Log.w(TAG, "Got an empty profile key while applying an account record update!");
|
||||
}
|
||||
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, update.getProfileKey().transform(Base64::encodeBytes).orNull());
|
||||
values.put(PROFILE_KEY, profileKey);
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
@@ -684,13 +741,28 @@ public class RecipientDatabase extends Database {
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static @NonNull ContentValues getValuesForStorageGroupV2(@NonNull SignalGroupV2Record groupV2) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKey()).toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<RecipientSettings> getRecipientSettings(@Nullable String query, @Nullable String[] args) {
|
||||
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
|
||||
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
|
||||
List<RecipientSettings> out = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
|
||||
String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION,
|
||||
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY });
|
||||
|
||||
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
out.add(getRecipientSettings(context, cursor));
|
||||
}
|
||||
@@ -722,10 +794,11 @@ public class RecipientDatabase extends Database {
|
||||
GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE)));
|
||||
byte[] key = Base64.decodeOrThrow(encodedKey);
|
||||
|
||||
if (groupType == GroupType.NONE) {
|
||||
out.put(id, StorageId.forContact(key));
|
||||
} else {
|
||||
out.put(id, StorageId.forGroupV1(key));
|
||||
switch (groupType) {
|
||||
case NONE : out.put(id, StorageId.forContact(key)); break;
|
||||
case SIGNAL_V1 : out.put(id, StorageId.forGroupV1(key)); break;
|
||||
case SIGNAL_V2 : out.put(id, StorageId.forGroupV2(key)); break;
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,6 +844,19 @@ public class RecipientDatabase extends Database {
|
||||
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
|
||||
|
||||
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
|
||||
GroupMasterKey groupMasterKey = null;
|
||||
try {
|
||||
if (masterKeyIndex != -1) {
|
||||
byte[] blob = cursor.getBlob(masterKeyIndex);
|
||||
if (blob != null) {
|
||||
groupMasterKey = new GroupMasterKey(blob);
|
||||
}
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
byte[] profileKeyCredential = null;
|
||||
@@ -805,7 +891,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw);
|
||||
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
VibrateState.fromId(messageVibrateState),
|
||||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
@@ -1356,22 +1442,20 @@ public class RecipientDatabase extends Database {
|
||||
Stream.of(updates.entrySet()).forEach(entry -> Recipient.live(entry.getKey()).refresh());
|
||||
}
|
||||
}
|
||||
public @Nullable Cursor getSignalContacts() {
|
||||
return getSignalContacts(true);
|
||||
}
|
||||
|
||||
public @Nullable Cursor getSignalContacts(boolean includeSelf) {
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
|
||||
"(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
||||
String[] args;
|
||||
|
||||
if (includeSelf) {
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1" };
|
||||
} else {
|
||||
selection += " AND " + ID + " != ?";
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), String.valueOf(Recipient.self().getId().toLong()) };
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", Recipient.self().getId().serialize() };
|
||||
}
|
||||
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
|
||||
@@ -1386,6 +1470,7 @@ public class RecipientDatabase extends Database {
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
|
||||
"(" +
|
||||
PHONE + " LIKE ? OR " +
|
||||
SORT_NAME + " LIKE ? OR " +
|
||||
@@ -1394,10 +1479,10 @@ public class RecipientDatabase extends Database {
|
||||
String[] args;
|
||||
|
||||
if (includeSelf) {
|
||||
args = new String[]{"0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query};
|
||||
args = new String[]{"0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query};
|
||||
} else {
|
||||
selection += " AND " + ID + " != ?";
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query, String.valueOf(Recipient.self().getId().toLong()) };
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query, String.valueOf(Recipient.self().getId().toLong()) };
|
||||
}
|
||||
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
|
||||
@@ -1644,6 +1729,17 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
private static ContentValues validateContactValuesForInsert(ContentValues values) {
|
||||
if (!FeatureFlags.uuids() &&
|
||||
values.getAsString(UUID) != null &&
|
||||
values.getAsString(PHONE) == null)
|
||||
{
|
||||
throw new UuidRecipientError();
|
||||
} else {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
public class BulkOperationsHandle {
|
||||
|
||||
private final SQLiteDatabase database;
|
||||
@@ -1734,6 +1830,7 @@ public class RecipientDatabase extends Database {
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final GroupId groupId;
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final GroupType groupType;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
@@ -1771,6 +1868,7 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String e164,
|
||||
@Nullable String email,
|
||||
@Nullable GroupId groupId,
|
||||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@NonNull GroupType groupType,
|
||||
boolean blocked,
|
||||
long muteUntil,
|
||||
@@ -1808,6 +1906,7 @@ public class RecipientDatabase extends Database {
|
||||
this.e164 = e164;
|
||||
this.email = email;
|
||||
this.groupId = groupId;
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.groupType = groupType;
|
||||
this.blocked = blocked;
|
||||
this.muteUntil = muteUntil;
|
||||
@@ -1864,6 +1963,13 @@ public class RecipientDatabase extends Database {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only read populated for sync.
|
||||
*/
|
||||
public @Nullable GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public @NonNull GroupType getGroupType() {
|
||||
return groupType;
|
||||
}
|
||||
@@ -2041,4 +2147,7 @@ public class RecipientDatabase extends Database {
|
||||
this.neededInsert = neededInsert;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UuidRecipientError extends AssertionError {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
|
||||
public final class ThreadBodyUtil {
|
||||
|
||||
private static final String TAG = Log.tag(ThreadBodyUtil.class);
|
||||
|
||||
private ThreadBodyUtil() {
|
||||
}
|
||||
|
||||
public static @NonNull String getFormattedBodyFor(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
return getFormattedBodyForMms(context, (MmsMessageRecord) record);
|
||||
}
|
||||
|
||||
return record.getBody();
|
||||
}
|
||||
|
||||
private static @NonNull String getFormattedBodyForMms(@NonNull Context context, @NonNull MmsMessageRecord record) {
|
||||
if (record.getSharedContacts().size() > 0) {
|
||||
Contact contact = record.getSharedContacts().get(0);
|
||||
|
||||
return ContactUtil.getStringSummary(context, contact).toString();
|
||||
} else if (record.getSlideDeck().getDocumentSlide() != null) {
|
||||
return format(context, record, EmojiStrings.FILE, R.string.ThreadRecord_file);
|
||||
} else if (record.getSlideDeck().getAudioSlide() != null) {
|
||||
return format(context, record, EmojiStrings.AUDIO, R.string.ThreadRecord_voice_message);
|
||||
} else if (MessageRecordUtil.hasSticker(record)) {
|
||||
return format(context, record, EmojiStrings.STICKER, R.string.ThreadRecord_sticker);
|
||||
}
|
||||
|
||||
boolean hasImage = false;
|
||||
boolean hasVideo = false;
|
||||
boolean hasGif = false;
|
||||
|
||||
for (Slide slide : record.getSlideDeck().getSlides()) {
|
||||
hasVideo |= slide.hasVideo();
|
||||
hasImage |= slide.hasImage();
|
||||
hasGif |= slide instanceof GifSlide;
|
||||
}
|
||||
|
||||
if (hasGif) {
|
||||
return format(context, record, EmojiStrings.GIF, R.string.ThreadRecord_gif);
|
||||
} else if (hasVideo) {
|
||||
return format(context, record, EmojiStrings.VIDEO, R.string.ThreadRecord_video);
|
||||
} else if (hasImage) {
|
||||
return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo);
|
||||
} else if (TextUtils.isEmpty(record.getBody())) {
|
||||
Log.w(TAG, "Got a media message without a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
|
||||
return context.getString(R.string.ThreadRecord_media_message);
|
||||
} else {
|
||||
Log.w(TAG, "Got a media message with a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
|
||||
return record.getBody();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull String format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
|
||||
return String.format("%s %s", emoji, getBodyOrDefault(context, record, defaultStringRes));
|
||||
}
|
||||
|
||||
private static @NonNull String getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
if (TextUtils.isEmpty(record.getBody())) {
|
||||
return context.getString(defaultStringRes);
|
||||
} else {
|
||||
return record.getBody();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,14 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -92,17 +94,27 @@ public class ThreadDatabase extends Database {
|
||||
public static final String LAST_SEEN = "last_seen";
|
||||
public static final String HAS_SENT = "has_sent";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
||||
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
|
||||
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + RECIPIENT_ID + " INTEGER, " + SNIPPET + " TEXT, " +
|
||||
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
|
||||
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
|
||||
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
||||
SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " + SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " +
|
||||
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
DATE + " INTEGER DEFAULT 0, " +
|
||||
MESSAGE_COUNT + " INTEGER DEFAULT 0, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
SNIPPET + " TEXT, " +
|
||||
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " +
|
||||
READ + " INTEGER DEFAULT " + ReadStatus.READ.serialize() + ", " +
|
||||
TYPE + " INTEGER DEFAULT 0, " +
|
||||
ERROR + " INTEGER DEFAULT 0, " +
|
||||
SNIPPET_TYPE + " INTEGER DEFAULT 0, " +
|
||||
SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
||||
SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " +
|
||||
SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " +
|
||||
ARCHIVED + " INTEGER DEFAULT 0, " +
|
||||
STATUS + " INTEGER DEFAULT 0, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
LAST_SEEN + " INTEGER DEFAULT 0, " +
|
||||
HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
|
||||
@@ -280,7 +292,7 @@ public class ThreadDatabase extends Database {
|
||||
public List<MarkedMessageInfo> setAllThreadsRead() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(READ, 1);
|
||||
contentValues.put(READ, ReadStatus.READ.serialize());
|
||||
contentValues.put(UNREAD_COUNT, 0);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, null, null);
|
||||
@@ -310,32 +322,69 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
public List<MarkedMessageInfo> setRead(long threadId, boolean lastSeen) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(READ, 1);
|
||||
contentValues.put(UNREAD_COUNT, 0);
|
||||
|
||||
if (lastSeen) {
|
||||
contentValues.put(LAST_SEEN, System.currentTimeMillis());
|
||||
}
|
||||
return setRead(Collections.singletonList(threadId), lastSeen);
|
||||
}
|
||||
|
||||
public List<MarkedMessageInfo> setRead(Collection<Long> threadIds, boolean lastSeen) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
||||
List<MarkedMessageInfo> smsRecords = new LinkedList<>();
|
||||
List<MarkedMessageInfo> mmsRecords = new LinkedList<>();
|
||||
|
||||
final List<MarkedMessageInfo> smsRecords = DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId);
|
||||
final List<MarkedMessageInfo> mmsRecords = DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId);
|
||||
db.beginTransaction();
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).setReactionsSeen(threadId);
|
||||
DatabaseFactory.getMmsDatabase(context).setReactionsSeen(threadId);
|
||||
try {
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(READ, ReadStatus.READ.serialize());
|
||||
contentValues.put(UNREAD_COUNT, 0);
|
||||
|
||||
if (lastSeen) {
|
||||
contentValues.put(LAST_SEEN, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{threadId + ""});
|
||||
|
||||
smsRecords.addAll(DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId));
|
||||
mmsRecords.addAll(DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId));
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).setReactionsSeen(threadId);
|
||||
DatabaseFactory.getMmsDatabase(context).setReactionsSeen(threadId);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
return Util.concatenatedList(smsRecords, mmsRecords);
|
||||
}
|
||||
|
||||
public void setForcedUnread(@NonNull Collection<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(READ, ReadStatus.FORCED_UNREAD.serialize());
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] { String.valueOf(threadId) });
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
|
||||
public void incrementUnread(long threadId, int amount) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = " + ReadStatus.UNREAD.serialize() + ", " +
|
||||
UNREAD_COUNT + " = " + UNREAD_COUNT + " + ? WHERE " + ID + " = ?",
|
||||
new String[] {String.valueOf(amount),
|
||||
String.valueOf(threadId)});
|
||||
@@ -708,7 +757,7 @@ public class ThreadDatabase extends Database {
|
||||
MessageRecord record;
|
||||
|
||||
if (reader != null && (record = reader.getNext()) != null) {
|
||||
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
|
||||
updateThread(threadId, count, ThreadBodyUtil.getFormattedBodyFor(context, record), getAttachmentUriFor(record),
|
||||
getContentTypeFor(record), getExtrasFor(record),
|
||||
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
|
||||
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
|
||||
@@ -725,15 +774,6 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getSharedContacts().size() > 0) {
|
||||
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
|
||||
return ContactUtil.getStringSummary(context, contact).toString();
|
||||
}
|
||||
|
||||
return messageRecord.getBody();
|
||||
}
|
||||
|
||||
private @Nullable Uri getAttachmentUriFor(MessageRecord record) {
|
||||
if (!record.isMms() || record.isMmsNotification() || record.isGroupAction()) return null;
|
||||
|
||||
@@ -848,31 +888,15 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
public ThreadRecord getCurrent() {
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
|
||||
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE));
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID)));
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID)));
|
||||
Recipient recipient = Recipient.live(recipientId).get();
|
||||
|
||||
Recipient recipient = Recipient.live(recipientId).get();
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
|
||||
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
|
||||
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
||||
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
|
||||
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
|
||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
|
||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
|
||||
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
|
||||
Uri snippetUri = getSnippetUri(cursor);
|
||||
String contentType = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_CONTENT_TYPE));
|
||||
String extraString = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_EXTRAS));
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0;
|
||||
}
|
||||
int readReceiptCount = TextSecurePreferences.isReadReceiptsEnabled(context) ? cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT))
|
||||
: 0;
|
||||
|
||||
Extra extra = null;
|
||||
String extraString = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_EXTRAS));
|
||||
Extra extra = null;
|
||||
|
||||
if (extraString != null) {
|
||||
try {
|
||||
@@ -882,9 +906,25 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
return new ThreadRecord(body, snippetUri, contentType, extra, recipient, date, count,
|
||||
unreadCount, threadId, deliveryReceiptCount, status, type,
|
||||
distributionType, archived, expiresIn, lastSeen, readReceiptCount);
|
||||
return new ThreadRecord.Builder(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)))
|
||||
.setRecipient(recipient)
|
||||
.setType(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)))
|
||||
.setDistributionType(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)))
|
||||
.setBody(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)))
|
||||
.setDate(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)))
|
||||
.setArchived(cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0)
|
||||
.setDeliveryStatus(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)))
|
||||
.setDeliveryReceiptCount(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT)))
|
||||
.setReadReceiptCount(readReceiptCount)
|
||||
.setExpiresIn(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)))
|
||||
.setLastSeen(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)))
|
||||
.setSnippetUri(getSnippetUri(cursor))
|
||||
.setContentType(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_CONTENT_TYPE)))
|
||||
.setCount(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)))
|
||||
.setUnreadCount(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)))
|
||||
.setForcedUnread(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)) == ReadStatus.FORCED_UNREAD.serialize())
|
||||
.setExtra(extra)
|
||||
.build();
|
||||
}
|
||||
|
||||
private @Nullable Uri getSnippetUri(Cursor cursor) {
|
||||
@@ -991,4 +1031,27 @@ public class ThreadDatabase extends Database {
|
||||
return groupAddedBy;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ReadStatus {
|
||||
READ(1), UNREAD(0), FORCED_UNREAD(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
ReadStatus(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ReadStatus deserialize(int value) {
|
||||
for (ReadStatus status : ReadStatus.values()) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No matching status for value " + value);
|
||||
}
|
||||
|
||||
public int serialize() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@@ -434,7 +434,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper {
|
||||
db.endTransaction();
|
||||
|
||||
// DecryptingQueue.schedulePendingDecrypts(context, masterSecret);
|
||||
MessageNotifier.updateNotification(context);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -56,7 +56,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
return context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(selfPending.get().getAddedByUuid()));
|
||||
}
|
||||
|
||||
if (group.getVersion() == 0) {
|
||||
if (group.getRevision() == 0) {
|
||||
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(group.getMembersList());
|
||||
if (foundingMember.isPresent()) {
|
||||
ByteString foundingMemberUuid = foundingMember.get().getUuid();
|
||||
@@ -140,16 +140,16 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (ByteString member : change.getDeleteMembersList()) {
|
||||
boolean newMemberIsYou = member.equals(selfUuidBytes);
|
||||
boolean removedMemberIsYou = member.equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_left_the_group));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_removed_s, describe(member)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_removed_you_from_the_group, describe(change.getEditor())));
|
||||
} else {
|
||||
if (member.equals(change.getEditor())) {
|
||||
|
||||
@@ -157,7 +157,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
|
||||
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getVersion() > 0) {
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() > 0) {
|
||||
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
|
||||
List<String> strings = updateMessageProducer.describeChange(change);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
||||
final class StatusUtil {
|
||||
private StatusUtil() {}
|
||||
|
||||
static boolean isDelivered(long deliveryStatus, int deliveryReceiptCount) {
|
||||
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
|
||||
deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||
}
|
||||
|
||||
static boolean isPending(long type) {
|
||||
return MmsSmsColumns.Types.isPendingMessageType(type) &&
|
||||
!MmsSmsColumns.Types.isIdentityVerified(type) &&
|
||||
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||
}
|
||||
|
||||
static boolean isFailed(long type, long deliveryStatus) {
|
||||
return MmsSmsColumns.Types.isFailedMessageType(type) ||
|
||||
MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) ||
|
||||
deliveryStatus >= SmsDatabase.Status.STATUS_FAILED;
|
||||
}
|
||||
|
||||
static boolean isVerificationStatusChange(long type) {
|
||||
return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type);
|
||||
}
|
||||
}
|
||||
@@ -17,151 +17,88 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase.Extra;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* The message record model which represents thread heading messages.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
* Represents an entry in the {@link org.thoughtcrime.securesms.database.ThreadDatabase}.
|
||||
*/
|
||||
public class ThreadRecord extends DisplayRecord {
|
||||
public final class ThreadRecord {
|
||||
|
||||
private @Nullable final Uri snippetUri;
|
||||
private @Nullable final String contentType;
|
||||
private @Nullable final Extra extra;
|
||||
private final long count;
|
||||
private final int unreadCount;
|
||||
private final int distributionType;
|
||||
private final boolean archived;
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
private final long threadId;
|
||||
private final String body;
|
||||
private final Recipient recipient;
|
||||
private final long type;
|
||||
private final long date;
|
||||
private final long deliveryStatus;
|
||||
private final int deliveryReceiptCount;
|
||||
private final int readReceiptCount;
|
||||
private final Uri snippetUri;
|
||||
private final String contentType;
|
||||
private final Extra extra;
|
||||
private final long count;
|
||||
private final int unreadCount;
|
||||
private final boolean forcedUnread;
|
||||
private final int distributionType;
|
||||
private final boolean archived;
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
|
||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||
@Nullable String contentType, @Nullable Extra extra,
|
||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||
long threadId, int deliveryReceiptCount, int status, long snippetType,
|
||||
int distributionType, boolean archived, long expiresIn, long lastSeen,
|
||||
int readReceiptCount)
|
||||
{
|
||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||
this.snippetUri = snippetUri;
|
||||
this.contentType = contentType;
|
||||
this.extra = extra;
|
||||
this.count = count;
|
||||
this.unreadCount = unreadCount;
|
||||
this.distributionType = distributionType;
|
||||
this.archived = archived;
|
||||
this.expiresIn = expiresIn;
|
||||
this.lastSeen = lastSeen;
|
||||
private ThreadRecord(@NonNull Builder builder) {
|
||||
this.threadId = builder.threadId;
|
||||
this.body = builder.body;
|
||||
this.recipient = builder.recipient;
|
||||
this.date = builder.date;
|
||||
this.type = builder.type;
|
||||
this.deliveryStatus = builder.deliveryStatus;
|
||||
this.deliveryReceiptCount = builder.deliveryReceiptCount;
|
||||
this.readReceiptCount = builder.readReceiptCount;
|
||||
this.snippetUri = builder.snippetUri;
|
||||
this.contentType = builder.contentType;
|
||||
this.extra = builder.extra;
|
||||
this.count = builder.count;
|
||||
this.unreadCount = builder.unreadCount;
|
||||
this.forcedUnread = builder.forcedUnread;
|
||||
this.distributionType = builder.distributionType;
|
||||
this.archived = builder.archived;
|
||||
this.expiresIn = builder.expiresIn;
|
||||
this.lastSeen = builder.lastSeen;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @Nullable Uri getSnippetUri() {
|
||||
return snippetUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (getGroupAddedBy() != null) {
|
||||
return emphasisAdded(context.getString(isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
Recipient.live(getGroupAddedBy()).get().getDisplayName(context)));
|
||||
} else if (!isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
|
||||
} else if (isGroupUpdate()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
} else if (isGroupQuit()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (isKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
} else if (SmsDatabase.Types.isEndSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
|
||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
||||
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
|
||||
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingCall(type)) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called_you));
|
||||
} else if (SmsDatabase.Types.isMissedCall(type)) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
|
||||
} else if (SmsDatabase.Types.isJoinedType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString(context)));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
||||
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
|
||||
if (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, getRecipient().toShortString(context)));
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
||||
} else if (SmsDatabase.Types.isUnsupportedMessageType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_could_not_be_processed));
|
||||
} else {
|
||||
if (TextUtils.isEmpty(getBody())) {
|
||||
if (extra != null && extra.isSticker()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
|
||||
} else if (extra != null && extra.isViewOnce()) {
|
||||
return new SpannableString(emphasisAdded(getViewOnceDescription(context, contentType)));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_this_message_was_deleted)));
|
||||
} else {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
||||
}
|
||||
} else {
|
||||
return new SpannableString(getBody());
|
||||
}
|
||||
}
|
||||
public @NonNull String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
private SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
public @Nullable Extra getExtra() {
|
||||
return extra;
|
||||
}
|
||||
|
||||
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
private String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
|
||||
if (MediaUtil.isViewOnceType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_view_once_media);
|
||||
} else if (MediaUtil.isVideoType(contentType)) {
|
||||
return context.getString(R.string.ThreadRecord_view_once_video);
|
||||
} else {
|
||||
return context.getString(R.string.ThreadRecord_view_once_photo);
|
||||
}
|
||||
public @Nullable String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
@@ -172,14 +109,26 @@ public class ThreadRecord extends DisplayRecord {
|
||||
return unreadCount;
|
||||
}
|
||||
|
||||
public boolean isForcedUnread() {
|
||||
return forcedUnread;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return unreadCount == 0 && !forcedUnread;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return getDateReceived();
|
||||
return date;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return archived;
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
@@ -192,6 +141,38 @@ public class ThreadRecord extends DisplayRecord {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingCall() {
|
||||
return SmsDatabase.Types.isOutgoingCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
return StatusUtil.isVerificationStatusChange(type);
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return StatusUtil.isPending(type);
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return StatusUtil.isFailed(type, deliveryStatus);
|
||||
}
|
||||
|
||||
public boolean isRemoteRead() {
|
||||
return readReceiptCount > 0;
|
||||
}
|
||||
|
||||
public boolean isPendingInsecureSmsFallback() {
|
||||
return SmsDatabase.Types.isPendingInsecureSmsFallbackType(type);
|
||||
}
|
||||
|
||||
public boolean isDelivered() {
|
||||
return StatusUtil.isDelivered(deliveryStatus, deliveryReceiptCount);
|
||||
}
|
||||
|
||||
public @Nullable RecipientId getGroupAddedBy() {
|
||||
if (extra != null && extra.getGroupAddedBy() != null) return RecipientId.from(extra.getGroupAddedBy());
|
||||
else return null;
|
||||
@@ -205,4 +186,129 @@ public class ThreadRecord extends DisplayRecord {
|
||||
if (extra != null) return extra.isMessageRequestAccepted();
|
||||
else return true;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private long threadId;
|
||||
private String body;
|
||||
private Recipient recipient;
|
||||
private long type;
|
||||
private long date;
|
||||
private long deliveryStatus;
|
||||
private int deliveryReceiptCount;
|
||||
private int readReceiptCount;
|
||||
private Uri snippetUri;
|
||||
private String contentType;
|
||||
private Extra extra;
|
||||
private long count;
|
||||
private int unreadCount;
|
||||
private boolean forcedUnread;
|
||||
private int distributionType;
|
||||
private boolean archived;
|
||||
private long expiresIn;
|
||||
private long lastSeen;
|
||||
|
||||
public Builder(long threadId) {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
|
||||
public Builder setBody(@NonNull String body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecipient(@NonNull Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setType(long type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setThreadId(long threadId) {
|
||||
this.threadId = threadId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDate(long date) {
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDeliveryStatus(long deliveryStatus) {
|
||||
this.deliveryStatus = deliveryStatus;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDeliveryReceiptCount(int deliveryReceiptCount) {
|
||||
this.deliveryReceiptCount = deliveryReceiptCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReadReceiptCount(int readReceiptCount) {
|
||||
this.readReceiptCount = readReceiptCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSnippetUri(@Nullable Uri snippetUri) {
|
||||
this.snippetUri = snippetUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setContentType(@Nullable String contentType) {
|
||||
this.contentType = contentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setExtra(@Nullable Extra extra) {
|
||||
this.extra = extra;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCount(long count) {
|
||||
this.count = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnreadCount(int unreadCount) {
|
||||
this.unreadCount = unreadCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setForcedUnread(boolean forcedUnread) {
|
||||
this.forcedUnread = forcedUnread;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDistributionType(int distributionType) {
|
||||
this.distributionType = distributionType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setArchived(boolean archived) {
|
||||
this.archived = archived;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setExpiresIn(long expiresIn) {
|
||||
this.expiresIn = expiresIn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLastSeen(long lastSeen) {
|
||||
this.lastSeen = lastSeen;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ThreadRecord build() {
|
||||
if (distributionType == ThreadDatabase.DistributionTypes.CONVERSATION) {
|
||||
Preconditions.checkArgument(threadId > 0);
|
||||
Preconditions.checkArgument(date > 0);
|
||||
Preconditions.checkNotNull(body);
|
||||
Preconditions.checkNotNull(recipient);
|
||||
}
|
||||
return new ThreadRecord(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@ package org.thoughtcrime.securesms.dependencies;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
|
||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
@@ -45,7 +49,7 @@ public class ApplicationDependencies {
|
||||
private static SignalServiceMessageSender messageSender;
|
||||
private static SignalServiceMessageReceiver messageReceiver;
|
||||
private static IncomingMessageProcessor incomingMessageProcessor;
|
||||
private static MessageRetriever messageRetriever;
|
||||
private static BackgroundMessageRetriever backgroundMessageRetriever;
|
||||
private static LiveRecipientCache recipientCache;
|
||||
private static JobManager jobManager;
|
||||
private static FrameRateTracker frameRateTracker;
|
||||
@@ -55,14 +59,18 @@ public class ApplicationDependencies {
|
||||
private static GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private static GroupsV2Operations groupsV2Operations;
|
||||
private static EarlyMessageCache earlyMessageCache;
|
||||
private static InitialMessageRetriever initialMessageRetriever;
|
||||
private static MessageNotifier messageNotifier;
|
||||
|
||||
@MainThread
|
||||
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
|
||||
throw new IllegalStateException("Already initialized!");
|
||||
}
|
||||
|
||||
ApplicationDependencies.application = application;
|
||||
ApplicationDependencies.provider = provider;
|
||||
ApplicationDependencies.application = application;
|
||||
ApplicationDependencies.provider = provider;
|
||||
ApplicationDependencies.messageNotifier = provider.provideMessageNotifier();
|
||||
}
|
||||
|
||||
public static @NonNull Application getApplication() {
|
||||
@@ -164,14 +172,14 @@ public class ApplicationDependencies {
|
||||
return incomingMessageProcessor;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull MessageRetriever getMessageRetriever() {
|
||||
public static synchronized @NonNull BackgroundMessageRetriever getBackgroundMessageRetriever() {
|
||||
assertInitialization();
|
||||
|
||||
if (messageRetriever == null) {
|
||||
messageRetriever = provider.provideMessageRetriever();
|
||||
if (backgroundMessageRetriever == null) {
|
||||
backgroundMessageRetriever = provider.provideBackgroundMessageRetriever();
|
||||
}
|
||||
|
||||
return messageRetriever;
|
||||
return backgroundMessageRetriever;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull LiveRecipientCache getRecipientCache() {
|
||||
@@ -234,6 +242,21 @@ public class ApplicationDependencies {
|
||||
return earlyMessageCache;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull InitialMessageRetriever getInitialMessageRetriever() {
|
||||
assertInitialization();
|
||||
|
||||
if (initialMessageRetriever == null) {
|
||||
initialMessageRetriever = provider.provideInitialMessageRetriever();
|
||||
}
|
||||
|
||||
return initialMessageRetriever;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull MessageNotifier getMessageNotifier() {
|
||||
assertInitialization();
|
||||
return messageNotifier;
|
||||
}
|
||||
|
||||
private static void assertInitialization() {
|
||||
if (application == null || provider == null) {
|
||||
throw new UninitializedException();
|
||||
@@ -247,13 +270,15 @@ public class ApplicationDependencies {
|
||||
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver();
|
||||
@NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess();
|
||||
@NonNull IncomingMessageProcessor provideIncomingMessageProcessor();
|
||||
@NonNull MessageRetriever provideMessageRetriever();
|
||||
@NonNull BackgroundMessageRetriever provideBackgroundMessageRetriever();
|
||||
@NonNull LiveRecipientCache provideRecipientCache();
|
||||
@NonNull JobManager provideJobManager();
|
||||
@NonNull FrameRateTracker provideFrameRateTracker();
|
||||
@NonNull KeyValueStore provideKeyValueStore();
|
||||
@NonNull MegaphoneRepository provideMegaphoneRepository();
|
||||
@NonNull EarlyMessageCache provideEarlyMessageCache();
|
||||
@NonNull InitialMessageRetriever provideInitialMessageRetriever();
|
||||
@NonNull MessageNotifier provideMessageNotifier();
|
||||
}
|
||||
|
||||
private static class UninitializedException extends IllegalStateException {
|
||||
|
||||
@@ -7,11 +7,11 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
@@ -20,15 +20,20 @@ import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
|
||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||
import org.thoughtcrime.securesms.push.SecurityEventListener;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.AlarmSleepTimer;
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
@@ -85,7 +90,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
Optional.fromNullable(IncomingMessageObserver.getPipe()),
|
||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
provideClientZkOperations().getProfileOperations());
|
||||
provideClientZkOperations().getProfileOperations(),
|
||||
SignalExecutors.UNBOUNDED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,8 +117,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageRetriever provideMessageRetriever() {
|
||||
return new MessageRetriever();
|
||||
public @NonNull BackgroundMessageRetriever provideBackgroundMessageRetriever() {
|
||||
return new BackgroundMessageRetriever();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,6 +158,16 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
return new EarlyMessageCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull InitialMessageRetriever provideInitialMessageRetriever() {
|
||||
return new InitialMessageRetriever();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageNotifier provideMessageNotifier() {
|
||||
return new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||
}
|
||||
|
||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.gcm;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
|
||||
import org.thoughtcrime.securesms.messages.RestStrategy;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* This service does the actual network fetch in response to an FCM message.
|
||||
*
|
||||
* Our goals with FCM processing are as follows:
|
||||
* (1) Ensure some service is active for the duration of the fetch and processing stages.
|
||||
* (2) Do not make unnecessary network requests.
|
||||
*
|
||||
* To fulfill goal 1, this service will not call {@link #stopSelf()} until there is no more running
|
||||
* requests.
|
||||
*
|
||||
* To fulfill goal 2, this service will not enqueue a fetch if there are already 2 active fetches
|
||||
* (or rather, 1 active and 1 waiting, since we use a single thread executor).
|
||||
*
|
||||
* Unfortunately we can't do this all in {@link FcmReceiveService} because it won't let us process
|
||||
* the next FCM message until {@link FcmReceiveService#onMessageReceived(RemoteMessage)} returns,
|
||||
* but as soon as that method returns, it could also destroy the service. By not letting us control
|
||||
* when the service is destroyed, we can't accomplish both goals within that service.
|
||||
*/
|
||||
public class FcmFetchService extends Service {
|
||||
|
||||
private static final String TAG = Log.tag(FcmFetchService.class);
|
||||
|
||||
private static final SerialMonoLifoExecutor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.UNBOUNDED);
|
||||
|
||||
private final AtomicInteger activeCount = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
boolean performedReplace = EXECUTOR.enqueue(this::fetch);
|
||||
|
||||
if (performedReplace) {
|
||||
Log.i(TAG, "Already have one running and one enqueued. Ignoring.");
|
||||
} else {
|
||||
int count = activeCount.incrementAndGet();
|
||||
Log.i(TAG, "Incrementing active count to " + count);
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.i(TAG, "onDestroy()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fetch() {
|
||||
retrieveMessages(this);
|
||||
|
||||
if (activeCount.decrementAndGet() == 0) {
|
||||
Log.e(TAG, "stopping");
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
static void retrieveMessages(@NonNull Context context) {
|
||||
BackgroundMessageRetriever retriever = ApplicationDependencies.getBackgroundMessageRetriever();
|
||||
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Successfully retrieved messages.");
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
Log.w(TAG, "Failed to retrieve messages. Scheduling on the system JobScheduler (API " + Build.VERSION.SDK_INT + ").");
|
||||
FcmJobService.schedule(context);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to retrieve messages. Scheduling on JobManager (API " + Build.VERSION.SDK_INT + ").");
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
|
||||
import org.thoughtcrime.securesms.messages.RestStrategy;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
/**
|
||||
* Pulls down messages. Used when we fail to pull down messages in {@link FcmService}.
|
||||
* Pulls down messages. Used when we fail to pull down messages in {@link FcmReceiveService}.
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
public class FcmJobService extends JobService {
|
||||
@@ -41,15 +41,15 @@ public class FcmJobService extends JobService {
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.d(TAG, "onStartJob()");
|
||||
|
||||
if (MessageRetriever.shouldIgnoreFetch(this)) {
|
||||
if (BackgroundMessageRetriever.shouldIgnoreFetch(this)) {
|
||||
Log.i(TAG, "App is foregrounded. No need to run.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Context context = getApplicationContext();
|
||||
MessageRetriever retriever = ApplicationDependencies.getMessageRetriever();
|
||||
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
|
||||
Context context = getApplicationContext();
|
||||
BackgroundMessageRetriever retriever = ApplicationDependencies.getBackgroundMessageRetriever();
|
||||
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Successfully retrieved messages.");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.gcm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -8,18 +9,17 @@ import androidx.annotation.NonNull;
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messages.RestStrategy;
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
public class FcmService extends FirebaseMessagingService {
|
||||
public class FcmReceiveService extends FirebaseMessagingService {
|
||||
|
||||
private static final String TAG = FcmService.class.getSimpleName();
|
||||
private static final String TAG = FcmReceiveService.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
@@ -29,7 +29,7 @@ public class FcmService extends FirebaseMessagingService {
|
||||
if (challenge != null) {
|
||||
handlePushChallenge(challenge);
|
||||
} else {
|
||||
handleReceivedNotification(getApplicationContext());
|
||||
handleReceivedNotification(ApplicationDependencies.getApplication());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class FcmService extends FirebaseMessagingService {
|
||||
public void onNewToken(String token) {
|
||||
Log.i(TAG, "onNewToken()");
|
||||
|
||||
if (!TextSecurePreferences.isPushRegistered(getApplicationContext())) {
|
||||
if (!TextSecurePreferences.isPushRegistered(ApplicationDependencies.getApplication())) {
|
||||
Log.i(TAG, "Got a new FCM token, but the user isn't registered.");
|
||||
return;
|
||||
}
|
||||
@@ -46,22 +46,12 @@ public class FcmService extends FirebaseMessagingService {
|
||||
}
|
||||
|
||||
private static void handleReceivedNotification(Context context) {
|
||||
MessageRetriever retriever = ApplicationDependencies.getMessageRetriever();
|
||||
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
|
||||
|
||||
if (success) {
|
||||
Log.i(TAG, "Successfully retrieved messages.");
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
Log.w(TAG, "Failed to retrieve messages. Scheduling on the system JobScheduler (API " + Build.VERSION.SDK_INT + ").");
|
||||
FcmJobService.schedule(context);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to retrieve messages. Scheduling on JobManager (API " + Build.VERSION.SDK_INT + ").");
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(context));
|
||||
}
|
||||
try {
|
||||
context.startService(new Intent(context, FcmFetchService.class));
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to start service. Falling back to legacy approach.");
|
||||
FcmFetchService.retrieveMessages(context);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Processing complete.");
|
||||
}
|
||||
|
||||
private static void handlePushChallenge(@NonNull String challenge) {
|
||||
@@ -69,4 +59,4 @@ public class FcmService extends FirebaseMessagingService {
|
||||
|
||||
PushChallengeRequest.postChallengeResponse(challenge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
public final class BadGroupIdException extends Exception {
|
||||
|
||||
public BadGroupIdException() {
|
||||
BadGroupIdException() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@@ -147,12 +147,13 @@ public final class GroupManager {
|
||||
@WorkerThread
|
||||
public static void updateGroupFromServer(@NonNull Context context,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
int version,
|
||||
long timestamp)
|
||||
int revision,
|
||||
long timestamp,
|
||||
@Nullable byte[] signedGroupChange)
|
||||
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
|
||||
updater.updateLocalToServerVersion(version, timestamp);
|
||||
updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +247,7 @@ public final class GroupManager {
|
||||
} else {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
|
||||
List<RecipientId> members = groupRecord.getMembers();
|
||||
byte[] avatar = Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId()));
|
||||
byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null;
|
||||
Set<RecipientId> addresses = new HashSet<>(members);
|
||||
|
||||
addresses.addAll(newMembers);
|
||||
|
||||
@@ -138,7 +138,7 @@ final class GroupManagerV1 {
|
||||
|
||||
if (avatar != null) {
|
||||
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null);
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
@@ -144,7 +146,7 @@ final class GroupManagerV2 {
|
||||
groupDatabase.onAvatarUpdated(groupId, avatar != null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||
|
||||
return sendGroupUpdate(masterKey, decryptedGroup, null);
|
||||
return sendGroupUpdate(masterKey, decryptedGroup, null, null);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new GroupChangeFailedException(e);
|
||||
}
|
||||
@@ -262,14 +264,9 @@ final class GroupManagerV2 {
|
||||
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull RecipientId recipientId)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
GroupManager.GroupActionResult result = commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.getUuid().get())));
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
groupDatabase.setActive(groupId, false);
|
||||
}
|
||||
|
||||
return result;
|
||||
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.getUuid().get())));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -354,7 +351,7 @@ final class GroupManagerV2 {
|
||||
throws IOException, GroupNotAMemberException, GroupChangeFailedException
|
||||
{
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
|
||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis());
|
||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
|
||||
|
||||
if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED || groupUpdateResult.getLatestServer() == null) {
|
||||
throw new GroupChangeFailedException();
|
||||
@@ -378,7 +375,7 @@ final class GroupManagerV2 {
|
||||
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
|
||||
final int nextRevision = v2GroupProperties.getGroupRevision() + 1;
|
||||
final GroupChange.Actions changeActions = change.setVersion(nextRevision).build();
|
||||
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
|
||||
final DecryptedGroupChange decryptedChange;
|
||||
final DecryptedGroup decryptedGroupState;
|
||||
|
||||
@@ -390,24 +387,24 @@ final class GroupManagerV2 {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
commitToServer(changeActions);
|
||||
GroupChange signedGroupChange = commitToServer(changeActions);
|
||||
groupDatabase.update(groupId, decryptedGroupState);
|
||||
|
||||
return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange);
|
||||
return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange);
|
||||
}
|
||||
|
||||
private void commitToServer(GroupChange.Actions change)
|
||||
private GroupChange commitToServer(GroupChange.Actions change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
groupsV2Api.patchGroup(change, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
|
||||
} catch (NotInGroupException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupNotAMemberException(e);
|
||||
} catch (AuthorizationFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupInsufficientRightsException(e);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupChangeFailedException(e);
|
||||
}
|
||||
@@ -430,11 +427,25 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void updateLocalToServerVersion(int version, long timestamp)
|
||||
void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
.updateLocalGroupToRevision(version, timestamp);
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
|
||||
}
|
||||
|
||||
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
|
||||
if (signedGroupChange != null) {
|
||||
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
||||
|
||||
try {
|
||||
return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Unable to verify supplied group change", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -445,11 +456,12 @@ final class GroupManagerV2 {
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
|
||||
@NonNull DecryptedGroup decryptedGroup,
|
||||
@Nullable DecryptedGroupChange plainGroupChange)
|
||||
@Nullable DecryptedGroupChange plainGroupChange,
|
||||
@Nullable GroupChange signedGroupChange)
|
||||
{
|
||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange);
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange, signedGroupChange);
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
||||
decryptedGroupV2Context,
|
||||
null,
|
||||
|
||||
@@ -8,17 +8,19 @@ import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -28,19 +30,19 @@ public final class GroupProtoUtil {
|
||||
private GroupProtoUtil() {
|
||||
}
|
||||
|
||||
public static int findVersionWeWereAdded(@NonNull DecryptedGroup group, @NonNull UUID uuid)
|
||||
public static int findRevisionWeWereAdded(@NonNull DecryptedGroup group, @NonNull UUID uuid)
|
||||
throws GroupNotAMemberException
|
||||
{
|
||||
ByteString bytes = UuidUtil.toByteString(uuid);
|
||||
for (DecryptedMember decryptedMember : group.getMembersList()) {
|
||||
if (decryptedMember.getUuid().equals(bytes)) {
|
||||
return decryptedMember.getJoinedAtVersion();
|
||||
return decryptedMember.getJoinedAtRevision();
|
||||
}
|
||||
}
|
||||
for (DecryptedPendingMember decryptedMember : group.getPendingMembersList()) {
|
||||
if (decryptedMember.getUuid().equals(bytes)) {
|
||||
// Assume latest, we don't have any information about when pending members were invited
|
||||
return group.getVersion();
|
||||
return group.getRevision();
|
||||
}
|
||||
}
|
||||
throw new GroupNotAMemberException();
|
||||
@@ -48,16 +50,20 @@ public final class GroupProtoUtil {
|
||||
|
||||
public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
|
||||
@NonNull DecryptedGroup decryptedGroup,
|
||||
@Nullable DecryptedGroupChange plainGroupChange)
|
||||
@Nullable DecryptedGroupChange plainGroupChange,
|
||||
@Nullable GroupChange signedServerChange)
|
||||
{
|
||||
int version = plainGroupChange != null ? plainGroupChange.getVersion() : decryptedGroup.getVersion();
|
||||
SignalServiceProtos.GroupContextV2 groupContext = SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(masterKey.serialize()))
|
||||
.setRevision(version)
|
||||
.build();
|
||||
int revision = plainGroupChange != null ? plainGroupChange.getRevision() : decryptedGroup.getRevision();
|
||||
SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(masterKey.serialize()))
|
||||
.setRevision(revision);
|
||||
|
||||
if (signedServerChange != null) {
|
||||
contextBuilder.setGroupChange(signedServerChange.toByteString());
|
||||
}
|
||||
|
||||
DecryptedGroupV2Context.Builder builder = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(groupContext)
|
||||
.setContext(contextBuilder.build())
|
||||
.setGroupState(decryptedGroup);
|
||||
|
||||
if (plainGroupChange != null) {
|
||||
@@ -83,6 +89,18 @@ public final class GroupProtoUtil {
|
||||
return Recipient.externalPush(context, uuid, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull RecipientId uuidByteStringToRecipientId(@NonNull ByteString uuidByteString) {
|
||||
UUID uuid = UUIDUtil.deserialize(uuidByteString.toByteArray());
|
||||
|
||||
if (uuid.equals(GroupsV2Operations.UNKNOWN_UUID)) {
|
||||
return RecipientId.UNKNOWN;
|
||||
}
|
||||
|
||||
return RecipientId.from(uuid, null);
|
||||
}
|
||||
|
||||
|
||||
public static boolean isMember(@NonNull UUID uuid, @NonNull List<DecryptedMember> membersList) {
|
||||
ByteString uuidBytes = UuidUtil.toByteString(uuid);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -252,7 +251,7 @@ public final class GroupV1MessageProcessor {
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId());
|
||||
return insertResult.get().getThreadId();
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -42,25 +42,46 @@ final class GroupsV2CapabilityChecker {
|
||||
Recipient member = Recipient.resolved(recipientId);
|
||||
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
|
||||
|
||||
if (gv2Capability == Recipient.Capability.UNKNOWN) {
|
||||
if (gv2Capability != Recipient.Capability.SUPPORTED) {
|
||||
if (!ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob.forRecipient(member), TimeUnit.SECONDS.toMillis(1000)).isPresent()) {
|
||||
throw new IOException("Recipient capability was not retrieved in time");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean noSelfGV2Support = false;
|
||||
int noGv2Count = 0;
|
||||
int noUuidCount = 0;
|
||||
|
||||
for (RecipientId recipientId : recipientIdsSet) {
|
||||
Recipient member = Recipient.resolved(recipientId);
|
||||
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
|
||||
|
||||
if (gv2Capability != Recipient.Capability.SUPPORTED) {
|
||||
Log.i(TAG, "At least one recipient does not support GV2, capability was " + gv2Capability);
|
||||
return false;
|
||||
Log.w(TAG, "At least one recipient does not support GV2, capability was " + gv2Capability);
|
||||
|
||||
noGv2Count++;
|
||||
if (member.isLocalNumber()) {
|
||||
noSelfGV2Support = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!member.hasUuid()) {
|
||||
noUuidCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (RecipientId recipientId : recipientIdsSet) {
|
||||
Recipient member = Recipient.resolved(recipientId);
|
||||
|
||||
if (!member.hasUuid()) {
|
||||
Log.i(TAG, "At least one recipient did not have a UUID known to us");
|
||||
return false;
|
||||
if (noGv2Count + noUuidCount > 0) {
|
||||
if (noUuidCount > 0) {
|
||||
Log.w(TAG, noUuidCount + " recipient(s) did not have a UUID known to us");
|
||||
}
|
||||
if (noGv2Count > 0) {
|
||||
Log.w(TAG, noGv2Count + " recipient(s) do not support GV2");
|
||||
if (noSelfGV2Support) {
|
||||
Log.w(TAG, "Self does not support GV2");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -29,8 +29,10 @@ public final class LiveGroup {
|
||||
|
||||
private static final Comparator<GroupMemberEntry.FullMember> LOCAL_FIRST = (m1, m2) -> Boolean.compare(m2.getMember().isLocalNumber(), m1.getMember().isLocalNumber());
|
||||
private static final Comparator<GroupMemberEntry.FullMember> ADMIN_FIRST = (m1, m2) -> Boolean.compare(m2.isAdmin(), m1.isAdmin());
|
||||
private static final Comparator<GroupMemberEntry.FullMember> ALPHABETICAL = (m1, m2) -> m1.getMember().toShortString(ApplicationDependencies.getApplication()).compareToIgnoreCase(m2.getMember().toShortString(ApplicationDependencies.getApplication()));
|
||||
private static final Comparator<? super GroupMemberEntry.FullMember> MEMBER_ORDER = ComparatorCompat.chain(LOCAL_FIRST)
|
||||
.thenComparing(ADMIN_FIRST);
|
||||
.thenComparing(ADMIN_FIRST)
|
||||
.thenComparing(ALPHABETICAL);
|
||||
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final LiveData<Recipient> recipient;
|
||||
@@ -91,11 +93,11 @@ public final class LiveGroup {
|
||||
}
|
||||
|
||||
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
||||
return LiveDataUtil.combineLatest(isSelfAdmin(), getAttributesAccessControl(), this::applyAccessControl);
|
||||
return LiveDataUtil.combineLatest(selfMemberLevel(), getAttributesAccessControl(), LiveGroup::applyAccessControl);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> selfCanAddMembers() {
|
||||
return LiveDataUtil.combineLatest(isSelfAdmin(), getMembershipAdditionAccessControl(), this::applyAccessControl);
|
||||
return LiveDataUtil.combineLatest(selfMemberLevel(), getMembershipAdditionAccessControl(), LiveGroup::applyAccessControl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,11 +123,28 @@ public final class LiveGroup {
|
||||
fullMemberCount);
|
||||
}
|
||||
|
||||
private boolean applyAccessControl(boolean isAdmin, @NonNull GroupAccessControl rights) {
|
||||
private LiveData<MemberLevel> selfMemberLevel() {
|
||||
return Transformations.map(groupRecord, g -> {
|
||||
if (g.isAdmin(Recipient.self())) {
|
||||
return MemberLevel.ADMIN;
|
||||
} else {
|
||||
return g.isActive() ? MemberLevel.MEMBER
|
||||
: MemberLevel.NOT_A_MEMBER;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean applyAccessControl(@NonNull MemberLevel memberLevel, @NonNull GroupAccessControl rights) {
|
||||
switch (rights) {
|
||||
case ALL_MEMBERS: return true;
|
||||
case ONLY_ADMINS: return isAdmin;
|
||||
case ALL_MEMBERS: return memberLevel != MemberLevel.NOT_A_MEMBER;
|
||||
case ONLY_ADMINS: return memberLevel == MemberLevel.ADMIN;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private enum MemberLevel {
|
||||
NOT_A_MEMBER,
|
||||
MEMBER,
|
||||
ADMIN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
public interface AddMembersResultCallback {
|
||||
void onMembersAdded(int numberOfMembersAdded);
|
||||
}
|
||||
@@ -35,8 +35,7 @@ public abstract class GroupMemberEntry {
|
||||
|
||||
public final static class NewGroupCandidate extends GroupMemberEntry {
|
||||
|
||||
private final DefaultValueLiveData<Boolean> isSelected = new DefaultValueLiveData<>(false);
|
||||
private final Recipient member;
|
||||
private final Recipient member;
|
||||
|
||||
public NewGroupCandidate(@NonNull Recipient member) {
|
||||
this.member = member;
|
||||
@@ -46,14 +45,6 @@ public abstract class GroupMemberEntry {
|
||||
return member;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Boolean> isSelected() {
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean isSelected) {
|
||||
this.isSelected.postValue(isSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
@@ -246,9 +246,6 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||
bindRecipient(newGroupCandidate.getMember());
|
||||
bindRecipientClick(newGroupCandidate.getMember());
|
||||
|
||||
itemView.setSelected(false);
|
||||
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
|
||||
|
||||
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
|
||||
|
||||
smsContact.setVisibility(smsWarningVisibility);
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class AddMembersActivity extends PushContactSelectionActivity {
|
||||
|
||||
public static final String GROUP_ID = "group_id";
|
||||
|
||||
private View done;
|
||||
private AlertDialog alert;
|
||||
private AddMembersViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_members_activity);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
AddMembersViewModel.Factory factory = new AddMembersViewModel.Factory(getGroupId());
|
||||
|
||||
done = findViewById(R.id.done);
|
||||
alert = buildConfirmationAlertDialog();
|
||||
viewModel = ViewModelProviders.of(this, factory)
|
||||
.get(AddMembersViewModel.class);
|
||||
|
||||
viewModel.getAddMemberDialogState().observe(this, state -> AddMembersActivity.updateAlertMessage(alert, state));
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
done.setOnClickListener(v -> {
|
||||
viewModel.setDialogStateForSelectedContacts(contactsFragment.getSelectedContacts());
|
||||
alert.show();
|
||||
});
|
||||
|
||||
disableDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeToolbar() {
|
||||
getToolbar().setNavigationIcon(R.drawable.ic_arrow_left_24);
|
||||
getToolbar().setNavigationOnClickListener(v -> {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= 1) {
|
||||
enableDone();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() < 1) {
|
||||
disableDone();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableDone() {
|
||||
done.setEnabled(true);
|
||||
done.animate().alpha(1f);
|
||||
}
|
||||
|
||||
private void disableDone() {
|
||||
done.setEnabled(false);
|
||||
done.animate().alpha(0.5f);
|
||||
}
|
||||
|
||||
private GroupId getGroupId() {
|
||||
return GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID));
|
||||
}
|
||||
|
||||
private AlertDialog buildConfirmationAlertDialog() {
|
||||
return new AlertDialog.Builder(this)
|
||||
.setMessage(" ")
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
onFinishedSelection();
|
||||
})
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
}
|
||||
|
||||
private static void updateAlertMessage(@NonNull AlertDialog alertDialog, @NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
|
||||
Context context = alertDialog.getContext();
|
||||
Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);
|
||||
|
||||
alertDialog.setMessage(context.getResources().getQuantityString(R.plurals.AddMembersActivity__add_d_members_to_s, state.getSelectionCount(),
|
||||
recipient.getDisplayName(context), state.getGroupTitle(), state.getSelectionCount()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
class AddMembersRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
AddMembersRepository() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
}
|
||||
|
||||
void getOrCreateRecipientId(@NonNull SelectedContact selectedContact, @NonNull Consumer<RecipientId> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> consumer.accept(selectedContact.getOrCreateRecipientId(context)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
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.R;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AddMembersViewModel extends ViewModel {
|
||||
|
||||
private final AddMembersRepository repository;
|
||||
private final LiveData<AddMemberDialogMessageState> addMemberDialogState;
|
||||
private final MutableLiveData<AddMemberDialogMessageStatePartial> partialState;
|
||||
|
||||
private AddMembersViewModel(@NonNull GroupId groupId) {
|
||||
repository = new AddMembersRepository();
|
||||
partialState = new MutableLiveData<>();
|
||||
addMemberDialogState = LiveDataUtil.combineLatest(Transformations.map(new LiveGroup(groupId).getTitle(), AddMembersViewModel::titleOrDefault),
|
||||
Transformations.switchMap(partialState, AddMembersViewModel::getStateWithoutGroupTitle),
|
||||
AddMembersViewModel::getStateWithGroupTitle);
|
||||
}
|
||||
|
||||
LiveData<AddMemberDialogMessageState> getAddMemberDialogState() {
|
||||
return addMemberDialogState;
|
||||
}
|
||||
|
||||
void setDialogStateForSelectedContacts(@NonNull List<SelectedContact> selectedContacts) {
|
||||
if (selectedContacts.size() == 1) {
|
||||
setDialogStateForSingleRecipient(selectedContacts.get(0));
|
||||
} else {
|
||||
setDialogStateForMultipleRecipients(selectedContacts.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void setDialogStateForSingleRecipient(@NonNull SelectedContact selectedContact) {
|
||||
//noinspection CodeBlock2Expr
|
||||
repository.getOrCreateRecipientId(selectedContact, recipientId -> {
|
||||
partialState.postValue(new AddMemberDialogMessageStatePartial(recipientId));
|
||||
});
|
||||
}
|
||||
|
||||
private void setDialogStateForMultipleRecipients(int recipientCount) {
|
||||
partialState.setValue(new AddMemberDialogMessageStatePartial(recipientCount));
|
||||
}
|
||||
|
||||
private static LiveData<AddMemberDialogMessageState> getStateWithoutGroupTitle(@NonNull AddMemberDialogMessageStatePartial partialState) {
|
||||
if (partialState.recipientId != null) {
|
||||
return Transformations.map(Recipient.live(partialState.recipientId).getLiveData(), r -> new AddMemberDialogMessageState(r, ""));
|
||||
} else {
|
||||
return new DefaultValueLiveData<>(new AddMemberDialogMessageState(partialState.memberCount, ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static AddMemberDialogMessageState getStateWithGroupTitle(@NonNull String title, @NonNull AddMemberDialogMessageState stateWithoutTitle) {
|
||||
return new AddMemberDialogMessageState(stateWithoutTitle.recipient, stateWithoutTitle.selectionCount, title);
|
||||
}
|
||||
|
||||
private static @NonNull String titleOrDefault(@Nullable String title) {
|
||||
return TextUtils.isEmpty(title) ? ApplicationDependencies.getApplication().getString(R.string.Recipient_unknown)
|
||||
: Objects.requireNonNull(title);
|
||||
}
|
||||
|
||||
private static final class AddMemberDialogMessageStatePartial {
|
||||
private final RecipientId recipientId;
|
||||
private final int memberCount;
|
||||
|
||||
private AddMemberDialogMessageStatePartial(@NonNull RecipientId recipientId) {
|
||||
this.recipientId = recipientId;
|
||||
this.memberCount = 1;
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageStatePartial(int memberCount) {
|
||||
Preconditions.checkArgument(memberCount > 1);
|
||||
this.memberCount = memberCount;
|
||||
this.recipientId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AddMemberDialogMessageState {
|
||||
private final Recipient recipient;
|
||||
private final String groupTitle;
|
||||
private final int selectionCount;
|
||||
|
||||
private AddMemberDialogMessageState(@NonNull Recipient recipient, @NonNull String groupTitle) {
|
||||
this(recipient, 1, groupTitle);
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageState(int selectionCount, @NonNull String groupTitle) {
|
||||
this(null, selectionCount, groupTitle);
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageState(@Nullable Recipient recipient, int selectionCount, @NonNull String groupTitle) {
|
||||
this.recipient = recipient;
|
||||
this.groupTitle = groupTitle;
|
||||
this.selectionCount = selectionCount;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getSelectionCount() {
|
||||
return selectionCount;
|
||||
}
|
||||
|
||||
public @NonNull String getGroupTitle() {
|
||||
return groupTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final GroupId groupId;
|
||||
|
||||
public Factory(@NonNull GroupId groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new AddMembersViewModel(groupId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, FeatureFlags.groupsV2create() ? FeatureFlags.gv2GroupCapacity() - 1
|
||||
: ContactSelectionListFragment.NO_LIMIT);
|
||||
|
||||
return intent;
|
||||
}
|
||||
@@ -81,6 +83,10 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SIZE) {
|
||||
enableNext();
|
||||
}
|
||||
@@ -88,6 +94,10 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() < MINIMUM_GROUP_SIZE) {
|
||||
disableNext();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.creategroup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public final class GroupSettingsCoordinatorLayoutBehavior extends CoordinatorLayout.Behavior<View> {
|
||||
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
|
||||
private final ViewRef avatarTargetRef = new ViewRef(R.id.avatar_target);
|
||||
private final ViewRef groupNameRef = new ViewRef(R.id.group_name);
|
||||
private final ViewRef groupNameTargetRef = new ViewRef(R.id.group_name_target);
|
||||
private final Rect targetRect = new Rect();
|
||||
private final Rect childRect = new Rect();
|
||||
|
||||
public GroupSettingsCoordinatorLayoutBehavior(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||
return dependency instanceof AppBarLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||
AppBarLayout appBarLayout = (AppBarLayout) dependency;
|
||||
int range = appBarLayout.getTotalScrollRange();
|
||||
float factor = INTERPOLATOR.getInterpolation(-appBarLayout.getY() / range);
|
||||
|
||||
updateAvatarPositionAndScale(parent, child, factor);
|
||||
updateNamePosition(parent, factor);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateAvatarPositionAndScale(@NonNull CoordinatorLayout parent, @NonNull View child, float factor) {
|
||||
View target = avatarTargetRef.require(parent);
|
||||
|
||||
targetRect.set(target.getLeft(), target.getTop(), target.getRight(), target.getBottom());
|
||||
childRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
|
||||
float widthScale = 1f - (1f - (targetRect.width() / (float) childRect.width())) * factor;
|
||||
float heightScale = 1f - (1f - (targetRect.height() / (float) childRect.height())) * factor;
|
||||
|
||||
float superimposedLeft = childRect.left + (childRect.width() - targetRect.width()) / 2f;
|
||||
float superimposedTop = childRect.top + (childRect.height() - targetRect.height()) / 2f;
|
||||
|
||||
float xTranslation = (targetRect.left - superimposedLeft) * factor;
|
||||
float yTranslation = (targetRect.top - superimposedTop) * factor;
|
||||
|
||||
child.setScaleX(widthScale);
|
||||
child.setScaleY(heightScale);
|
||||
child.setTranslationX(xTranslation);
|
||||
child.setTranslationY(yTranslation);
|
||||
}
|
||||
|
||||
private void updateNamePosition(@NonNull CoordinatorLayout parent, float factor) {
|
||||
TextView child = (TextView) groupNameRef.require(parent);
|
||||
View target = groupNameTargetRef.require(parent);
|
||||
|
||||
targetRect.set(target.getLeft(), target.getTop(), target.getRight(), target.getBottom());
|
||||
childRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
|
||||
if (child.getMaxWidth() != targetRect.width()) {
|
||||
child.setMaxWidth(targetRect.width());
|
||||
}
|
||||
|
||||
float deltaTop = targetRect.top - childRect.top;
|
||||
float deltaStart = getStart(parent, targetRect) - getStart(parent, childRect);
|
||||
|
||||
float yTranslation = deltaTop * factor;
|
||||
float xTranslation = deltaStart * factor;
|
||||
|
||||
child.setTranslationY(yTranslation);
|
||||
child.setTranslationX(xTranslation);
|
||||
}
|
||||
|
||||
private static int getStart(@NonNull CoordinatorLayout parent, @NonNull Rect rect) {
|
||||
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? rect.left : rect.right;
|
||||
}
|
||||
|
||||
private static final class ViewRef {
|
||||
|
||||
private WeakReference<View> ref = new WeakReference<>(null);
|
||||
|
||||
private final @IdRes int idRes;
|
||||
|
||||
private ViewRef(@IdRes int idRes) {
|
||||
this.idRes = idRes;
|
||||
}
|
||||
|
||||
private @NonNull View require(@NonNull View parent) {
|
||||
View view = ref.get();
|
||||
|
||||
if (view == null) {
|
||||
view = getChildOrThrow(parent, idRes);
|
||||
ref = new WeakReference<>(view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private static @NonNull View getChildOrThrow(@NonNull View parent, @IdRes int id) {
|
||||
View child = parent.findViewById(id);
|
||||
|
||||
if (child == null) {
|
||||
throw new AssertionError("Can't find view with ID " + R.id.avatar_target);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,8 @@ public class AddGroupDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
recipientId,
|
||||
threadId,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
-1);
|
||||
-1,
|
||||
false);
|
||||
|
||||
startActivity(intent);
|
||||
setResult(RESULT_OK);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -20,6 +21,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
@@ -37,6 +39,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -58,38 +61,6 @@ public class AddGroupDetailsFragment extends Fragment {
|
||||
private Drawable avatarPlaceholder;
|
||||
private EditText name;
|
||||
private Toolbar toolbar;
|
||||
private ActionMode actionMode;
|
||||
|
||||
private ActionMode.Callback recipientActionModeCallback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.add_group_details_fragment_context_menu, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_delete) {
|
||||
viewModel.deleteSelected();
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
viewModel.clearSelected();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -140,16 +111,19 @@ public class AddGroupDetailsFragment extends Fragment {
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
avatar.setOnClickListener(v -> AvatarSelectionBottomSheetDialogFragment.create(false, true, REQUEST_CODE_AVATAR, true)
|
||||
.show(getChildFragmentManager(), "BOTTOM"));
|
||||
members.setRecipientLongClickListener(this::handleRecipientLongClick);
|
||||
avatar.setOnClickListener(v -> showAvatarSelectionBottomSheet());
|
||||
members.setRecipientClickListener(this::handleRecipientClick);
|
||||
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.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
|
||||
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE));
|
||||
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
|
||||
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
|
||||
name.setVisibility(isMms ? View.GONE : View.VISIBLE);
|
||||
avatar.setVisibility(isMms ? View.GONE : View.VISIBLE);
|
||||
toolbar.setTitle(isMms ? R.string.AddGroupDetailsFragment__create_group : R.string.AddGroupDetailsFragment__name_this_group);
|
||||
});
|
||||
viewModel.getAvatar().observe(getViewLifecycleOwner(), avatarBytes -> {
|
||||
if (avatarBytes == null) {
|
||||
avatar.setImageDrawable(new InsetDrawable(avatarPlaceholder, ViewUtil.dpToPx(AVATAR_PLACEHOLDER_INSET_DP)));
|
||||
@@ -162,11 +136,19 @@ public class AddGroupDetailsFragment extends Fragment {
|
||||
.into(avatar);
|
||||
}
|
||||
});
|
||||
|
||||
name.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == REQUEST_CODE_AVATAR && resultCode == Activity.RESULT_OK && data != null) {
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
viewModel.setAvatar(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||
final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
|
||||
|
||||
@@ -211,29 +193,15 @@ public class AddGroupDetailsFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void handleRecipientClick(@NonNull Recipient recipient) {
|
||||
if (actionMode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int size = viewModel.toggleSelected(recipient);
|
||||
if (size == 0) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleRecipientLongClick(@NonNull Recipient recipient) {
|
||||
if (actionMode != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
actionMode = toolbar.startActionMode(recipientActionModeCallback);
|
||||
|
||||
if (actionMode != null) {
|
||||
viewModel.toggleSelected(recipient);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(getString(R.string.AddGroupDetailsFragment__remove_s_from_this_group, recipient.getDisplayName(requireContext())))
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
|
||||
.setPositiveButton(R.string.AddGroupDetailsFragment__remove, (dialog, which) -> {
|
||||
viewModel.delete(recipient.getId());
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) {
|
||||
@@ -280,6 +248,15 @@ public class AddGroupDetailsFragment extends Fragment {
|
||||
.alpha(isEnabled ? 1f : 0.5f);
|
||||
}
|
||||
|
||||
private void showAvatarSelectionBottomSheet() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(() -> AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_AVATAR, true)
|
||||
.show(getChildFragmentManager(), "BOTTOM"))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onGroupCreated(@NonNull RecipientId recipientId, long threadId);
|
||||
void onNavigationButtonPressed();
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups.ui.creategroup.details;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
@@ -13,7 +14,6 @@ import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
@@ -27,27 +27,25 @@ import java.util.Set;
|
||||
public final class AddGroupDetailsViewModel extends ViewModel {
|
||||
|
||||
private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members;
|
||||
private final DefaultValueLiveData<Set<RecipientId>> selected = new DefaultValueLiveData<>(new HashSet<>());
|
||||
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
|
||||
private final MutableLiveData<String> name = new MutableLiveData<>("");
|
||||
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
|
||||
private final LiveData<Boolean> isMms;
|
||||
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
|
||||
private final LiveData<Boolean> canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name));
|
||||
private final LiveData<Boolean> isMms;
|
||||
private final LiveData<Boolean> canSubmitForm;
|
||||
private final AddGroupDetailsRepository repository;
|
||||
|
||||
AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
|
||||
@NonNull AddGroupDetailsRepository repository)
|
||||
private AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
|
||||
@NonNull AddGroupDetailsRepository repository)
|
||||
{
|
||||
this.repository = repository;
|
||||
|
||||
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
|
||||
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersWithoutDeleted = LiveDataUtil.combineLatest(initialMembers,
|
||||
deleted,
|
||||
AddGroupDetailsViewModel::filterDeletedMembers);
|
||||
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
|
||||
|
||||
members = LiveDataUtil.combineLatest(membersWithoutDeleted, selected, AddGroupDetailsViewModel::updateSelectedMembers);
|
||||
isMms = Transformations.map(members, this::isAnyForcedSms);
|
||||
LiveData<Boolean> isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
|
||||
members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
|
||||
isMms = Transformations.map(members, this::isAnyForcedSms);
|
||||
canSubmitForm = LiveDataUtil.combineLatest(isMms, isValidName, (mms, validName) -> mms || validName);
|
||||
|
||||
repository.resolveMembers(recipientIds, initialMembers::postValue);
|
||||
}
|
||||
@@ -72,35 +70,22 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||
return isMms;
|
||||
}
|
||||
|
||||
void setAvatar(@NonNull byte[] avatar) {
|
||||
void setAvatar(@Nullable byte[] avatar) {
|
||||
this.avatar.setValue(avatar);
|
||||
}
|
||||
|
||||
boolean hasAvatar() {
|
||||
return avatar.getValue() != null;
|
||||
}
|
||||
|
||||
void setName(@NonNull String name) {
|
||||
this.name.setValue(name);
|
||||
}
|
||||
|
||||
int toggleSelected(@NonNull Recipient recipient) {
|
||||
Set<RecipientId> selected = this.selected.getValue();
|
||||
|
||||
if (!selected.add(recipient.getId())) {
|
||||
selected.remove(recipient.getId());
|
||||
}
|
||||
|
||||
this.selected.setValue(selected);
|
||||
|
||||
return selected.size();
|
||||
}
|
||||
|
||||
void clearSelected() {
|
||||
this.selected.setValue(new HashSet<>());
|
||||
}
|
||||
|
||||
void deleteSelected() {
|
||||
Set<RecipientId> selected = this.selected.getValue();
|
||||
void delete(@NonNull RecipientId recipientId) {
|
||||
Set<RecipientId> deleted = this.deleted.getValue();
|
||||
|
||||
deleted.addAll(selected);
|
||||
deleted.add(recipientId);
|
||||
this.deleted.setValue(deleted);
|
||||
}
|
||||
|
||||
@@ -108,10 +93,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
|
||||
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
|
||||
byte[] avatarBytes = avatar.getValue();
|
||||
String groupName = name.getValue();
|
||||
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
|
||||
String groupName = isGroupMms ? "" : name.getValue();
|
||||
|
||||
if (TextUtils.isEmpty(groupName)) {
|
||||
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
|
||||
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
|
||||
return;
|
||||
}
|
||||
@@ -134,14 +119,6 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static @NonNull List<GroupMemberEntry.NewGroupCandidate> updateSelectedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> selected) {
|
||||
for (GroupMemberEntry.NewGroupCandidate member : members) {
|
||||
member.setSelected(selected.contains(member.getMember().getId()));
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
private boolean isAnyForcedSms(@NonNull List<GroupMemberEntry.NewGroupCandidate> members) {
|
||||
return Stream.of(members)
|
||||
.anyMatch(member -> !member.getMember().isRegistered());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user