diff --git a/.gitignore b/.gitignore
index c609669e9..1ded40463 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ obj/
jni/libspeex/.deps/
*.sh
pkcs11.password
+dev.keystore
diff --git a/app/build.gradle b/app/build.gradle
index adb7b2633..5cefba9a3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -80,8 +80,8 @@ protobuf {
}
}
-def canonicalVersionCode = 708
-def canonicalVersionName = "4.71.5"
+def canonicalVersionCode = 709
+def canonicalVersionName = "4.72.0"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -100,6 +100,15 @@ android {
javaMaxHeapSize "4g"
}
+ signingConfigs {
+ staging {
+ storeFile file("${project.rootDir}/dev.keystore")
+ storePassword 'android'
+ keyAlias 'staging'
+ keyPassword 'android'
+ }
+ }
+
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
@@ -195,6 +204,8 @@ android {
}
staging {
initWith debug
+ applicationIdSuffix ".staging"
+ signingConfig signingConfigs.staging
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
@@ -310,7 +321,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
- implementation 'org.signal:ringrtc-android:2.5.1'
+ implementation 'org.signal:ringrtc-android:2.7.0'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
diff --git a/app/src/flipper/AndroidManifest.xml b/app/src/flipper/AndroidManifest.xml
index 1f15fab86..e2404f13d 100644
--- a/app/src/flipper/AndroidManifest.xml
+++ b/app/src/flipper/AndroidManifest.xml
@@ -5,5 +5,12 @@
+ tools:replace="android:name">
+
+
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 298775a11..4e50e84d1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,7 +5,7 @@
-
@@ -113,7 +113,7 @@
-
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
+ android:value=".MainActivity" />
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -155,7 +155,7 @@
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -195,7 +195,7 @@
android:launchMode="singleTask"
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
- android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -242,7 +242,7 @@
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -343,45 +343,45 @@
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
- android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -420,7 +420,7 @@
android:excludeFromRecents="true"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
- android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -432,15 +432,15 @@
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -474,39 +474,39 @@
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
@@ -521,14 +521,14 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
-
+
@@ -650,15 +650,15 @@
+ android:authorities="${applicationId}.part" />
+ android:authorities="${applicationId}.mms" />
@@ -667,23 +667,23 @@
diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java
index a6dd4e5ea..1227a4486 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities {
@@ -9,12 +8,13 @@ public final class AppCapabilities {
}
private static final boolean UUID_CAPABLE = false;
+ private static final boolean GV2_CAPABLE = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
- public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
- return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
+ public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
+ return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index b1a13a1eb..0b2e8595d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -137,7 +137,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
- RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
@@ -155,6 +154,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
+ RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/AvatarPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/AvatarPreviewActivity.java
index 3156dda22..4fa1ada7a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/AvatarPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/AvatarPreviewActivity.java
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms;
-import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -10,12 +9,7 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.transition.TransitionInflater;
-import android.view.DisplayCutout;
import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.Window;
-import android.view.WindowManager;
import android.widget.ImageView;
import androidx.annotation.NonNull;
@@ -40,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.FullscreenHelper;
/**
* Activity for displaying avatars full screen.
@@ -81,17 +76,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
setSupportActionBar(toolbar);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
-
- if (Build.VERSION.SDK_INT >= 28) {
- getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new DisplayCutoutAdjuster(toolbar, findViewById(R.id.toolbar_cutout_spacer)));
- }
-
- showSystemUI();
-
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
Context context = getApplicationContext();
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
@@ -140,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
toolbar.setTitle(recipient.getDisplayName(context));
});
- avatar.setOnClickListener(v -> toggleUiVisibility());
+ FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
- showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
- }
+ findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
- private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
- window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
- boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
+ fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
- 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 );
+ fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
}
@Override
@@ -188,36 +139,4 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
onBackPressed();
return true;
}
-
- /**
- * Adjust a spacer for the toolbar when a display cutout is detected. Runs within
- * a layout listener because the activity delays view attachment due to the transitions
- * and needs to update on device rotation.
- */
- @TargetApi(28)
- private static class DisplayCutoutAdjuster implements ViewTreeObserver.OnGlobalLayoutListener {
-
- private final View view;
- private final View spacer;
-
- private DisplayCutoutAdjuster(@NonNull View view, @NonNull View spacer) {
- this.view = view;
- this.spacer = spacer;
- }
-
- @Override
- public void onGlobalLayout() {
- if (view.getRootWindowInsets() == null) {
- return;
- }
-
- DisplayCutout cutout = view.getRootWindowInsets().getDisplayCutout();
- if (cutout != null) {
- ViewGroup.LayoutParams params = spacer.getLayoutParams();
- params.height = cutout.getSafeInsetTop();
- spacer.setLayoutParams(params);
- spacer.setVisibility(View.VISIBLE);
- }
- }
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
index 81a7f14db..b77ab1d1b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -4,6 +4,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
@@ -22,7 +23,8 @@ import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
- void bind(@NonNull ConversationMessage messageRecord,
+ void bind(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull ConversationMessage messageRecord,
@NonNull Optional previousMessageRecord,
@NonNull Optional nextMessageRecord,
@NonNull GlideRequests glideRequests,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 7c1287bc1..5c66327b2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -31,8 +31,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
@@ -69,6 +67,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
+import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
@@ -119,6 +118,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private boolean cameFromAllMedia;
private boolean showThread;
private MediaDatabase.Sorting sorting;
+ private FullscreenHelper fullscreenHelper;
private @Nullable Cursor cursor = null;
@@ -133,7 +133,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
- intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType());
+ intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent;
}
@@ -147,10 +147,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
-
- showSystemUI();
+ fullscreenHelper = new FullscreenHelper(this);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -273,9 +270,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer);
- anchorMarginsToTopInsets(toolbarLayout);
+ fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
- showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
+ fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
}
private void initializeResources() {
@@ -546,7 +543,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@Override
public boolean singleTapOnMedia() {
- toggleUiVisibility();
+ fullscreenHelper.toggleUiVisibility();
return true;
}
@@ -556,32 +553,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish();
}
- 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 );
- }
-
private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override
@@ -697,33 +668,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
});
}
- private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
- ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
- ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
-
- layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
- insets.getSystemWindowInsetTop(),
- insets.getSystemWindowInsetRight(),
- layoutParams.bottomMargin);
-
- view.setLayoutParams(layoutParams);
-
- return insets;
- });
- }
-
- 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 static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
@SuppressLint("UseSparseArrays")
@@ -801,7 +745,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(),
attachment,
- Objects.requireNonNull(attachment.getDataUri()),
+ Objects.requireNonNull(attachment.getUri()),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.isOutgoing());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
index 9caa5662b..18553dae1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -65,16 +66,17 @@ public class NewConversationActivity extends ContactSelectionActivity
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
- if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
- Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh.");
+
+ if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
+ Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number);
- if (!resolved.isRegistered()) {
- Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh.");
+ if (!resolved.isRegistered() || !resolved.hasUuid()) {
+ Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId());
@@ -102,7 +104,7 @@ public class NewConversationActivity extends ContactSelectionActivity
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
intent.setDataAndType(getIntent().getData(), getIntent().getType());
- long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
+ long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java b/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java
index 28c55b6c2..58d55f7f0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else {
Recipient recipient = Recipient.external(this, destination.getDestination());
- long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
+ long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
nextIntent = new Intent(this, ConversationActivity.class);
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java
index 24dac2be5..f0a3ef251 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java
@@ -19,8 +19,6 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.PictureInPictureParams;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@@ -41,9 +39,11 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup;
+import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
+import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
@@ -52,11 +52,12 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
+import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
+import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
@@ -85,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
+ //noinspection ConstantConditions
getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@@ -132,11 +134,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
super.onStop();
EventBus.getDefault().unregister(this);
- }
- @Override
- public void onConfigurationChanged(Configuration newConfiguration) {
- super.onConfigurationChanged(newConfiguration);
+ CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
+ if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
+ Intent intent = new Intent(this, WebRtcCallService.class);
+ intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
+ startService(intent);
+ }
}
@Override
@@ -162,7 +166,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private boolean enterPipModeIfPossible() {
- if (isSystemPipEnabledAndAvailable()) {
+ if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16))
.build();
@@ -196,21 +200,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private void initializeResources() {
- callScreen = ViewUtil.findById(this, R.id.callScreen);
+ callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode());
- viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
- viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
- viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
- viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
+ viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@@ -375,19 +376,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent);
}
- private void handleIncomingCall(@NonNull WebRtcViewModel event) {
- callScreen.setRecipient(event.getRecipient());
- }
-
- private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
- callScreen.setRecipient(event.getRecipient());
+ private void handleOutgoingCall() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name());
- callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
@@ -398,62 +393,47 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish();
}
- private void handleCallRinging(@NonNull WebRtcViewModel event) {
- callScreen.setRecipient(event.getRecipient());
+ private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
- private void handleCallBusy(@NonNull WebRtcViewModel event) {
+ private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
}
- private void handleCallConnected(@NonNull WebRtcViewModel event) {
+ private void handleCallConnected() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
- callScreen.setRecipient(event.getRecipient());
}
- private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
+ private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
- private void handleServerFailure(@NonNull WebRtcViewModel event) {
+ private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
- callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
- AlertDialog.Builder dialog = new AlertDialog.Builder(this);
- dialog.setTitle(R.string.RedPhone_number_not_registered);
- dialog.setIconAttribute(R.attr.dialog_alert_icon);
- dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
- dialog.setCancelable(true);
- dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
- }
- });
- dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialog) {
- WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
- }
- });
- dialog.show();
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.RedPhone_number_not_registered)
+ .setIconAttribute(R.attr.dialog_alert_icon)
+ .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
+ .setCancelable(true)
+ .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
+ .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
+ .show();
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
- final IdentityKey theirKey = event.getIdentityKey();
- final Recipient recipient = event.getRecipient();
+ final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
+ final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL);
@@ -493,32 +473,29 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
- public void onEventMainThread(final WebRtcViewModel event) {
+ public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient());
+ callScreen.setRecipient(event.getRecipient());
switch (event.getState()) {
- case CALL_CONNECTED: handleCallConnected(event); break;
- case NETWORK_FAILURE: handleServerFailure(event); break;
- case CALL_RINGING: handleCallRinging(event); break;
+ case CALL_CONNECTED: handleCallConnected(); break;
+ case NETWORK_FAILURE: handleServerFailure(); break;
+ case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
- case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
- case CALL_INCOMING: handleIncomingCall(event); break;
- case CALL_OUTGOING: handleOutgoingCall(event); break;
- case CALL_BUSY: handleCallBusy(event); break;
+ case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
+ case CALL_OUTGOING: handleOutgoingCall(); break;
+ case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
- callScreen.setLocalRenderer(event.getLocalRenderer());
- callScreen.setRemoteRenderer(event.getRemoteRenderer());
-
- boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
+ boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
viewModel.updateFromWebRtcViewModel(event, enableVideo);
@@ -530,6 +507,24 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private final class ControlsListener implements WebRtcCallView.ControlsListener {
+ @Override
+ public void onStartCall(boolean isVideoCall) {
+ enableVideoIfAvailable = isVideoCall;
+
+ Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
+ intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
+ .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
+ .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
+ startService(intent);
+
+ MessageSender.onMessageSent();
+ }
+
+ @Override
+ public void onCancelStartCall() {
+ finish();
+ }
+
@Override
public void onControlsFadeOut() {
if (videoTooltip != null) {
@@ -594,8 +589,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
@Override
- public void onDownCaretPressed() {
+ public void onShowParticipantsList() {
+ CallParticipantsListDialog.show(getSupportFragmentManager());
+ }
+ @Override
+ public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
+ viewModel.setIsViewingFocusedParticipant(page);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java b/app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java
new file mode 100644
index 000000000..530848439
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java
@@ -0,0 +1,50 @@
+package org.thoughtcrime.securesms.animation;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import androidx.annotation.NonNull;
+
+public class ResizeAnimation extends Animation {
+
+ private final View target;
+ private final int targetWidthPx;
+ private final int targetHeightPx;
+
+ private int startWidth;
+ private int startHeight;
+
+ public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
+ this.target = target;
+ this.targetWidthPx = targetWidthPx;
+ this.targetHeightPx = targetHeightPx;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
+ int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
+
+ ViewGroup.LayoutParams params = target.getLayoutParams();
+
+ params.width = newWidth;
+ params.height = newHeight;
+
+ target.setLayoutParams(params);
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+
+ this.startWidth = width;
+ this.startHeight = height;
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java
index 0ae74bdba..90696f2ce 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java
@@ -106,10 +106,7 @@ public abstract class Attachment {
}
@Nullable
- public abstract Uri getDataUri();
-
- @Nullable
- public abstract Uri getThumbnailUri();
+ public abstract Uri getUri();
public int getTransferState() {
return transferState;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
index f294b9d98..023211642 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
@@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
@Override
@Nullable
- public Uri getDataUri() {
+ public Uri getUri() {
if (hasData) {
return PartAuthority.getAttachmentDataUri(attachmentId);
} else {
@@ -65,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
}
}
- @Override
- @Nullable
- public Uri getThumbnailUri() {
- if (hasThumbnail) {
- return PartAuthority.getAttachmentThumbnailUri(attachmentId);
- } else {
- return null;
- }
- }
-
public AttachmentId getAttachmentId() {
return attachmentId;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
index edcad15d9..3eda94374 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
@@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
@Nullable
@Override
- public Uri getDataUri() {
- return null;
- }
-
- @Nullable
- @Override
- public Uri getThumbnailUri() {
+ public Uri getUri() {
return null;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java
index 6aeb5f7a4..3454922c7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java
@@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
@Nullable
@Override
- public Uri getDataUri() {
+ public Uri getUri() {
return null;
}
- @Nullable
- @Override
- public Uri getThumbnailUri() {
- return null;
- }
-
-
public static List forPointers(Optional> pointers) {
List results = new LinkedList<>();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java
index 4b1af3ec9..4cdee54b8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java
@@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
}
@Override
- public @Nullable Uri getDataUri() {
- return null;
- }
-
- @Override
- public @Nullable Uri getThumbnailUri() {
+ public @Nullable Uri getUri() {
return null;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java
index 15dbff49c..cc28d760d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
- private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri,
@NonNull String contentType,
@@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
- this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
+ this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
}
public UriAttachment(@NonNull Uri dataUri,
- @Nullable Uri thumbnailUri,
@NonNull String contentType,
int transferState,
long size,
@@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
- this.dataUri = dataUri;
- this.thumbnailUri = thumbnailUri;
+ this.dataUri = dataUri;
}
@Override
@NonNull
- public Uri getDataUri() {
+ public Uri getUri() {
return dataUri;
}
- @Override
- @Nullable
- public Uri getThumbnailUri() {
- return thumbnailUri;
- }
-
@Override
public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
index 34f416a81..8a3e616a1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
@@ -138,13 +138,11 @@ public class FullBackupImporter extends FullBackupBase {
inputStream.readAttachmentTo(output.second, attachment.getLength());
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
- contentValues.put(AttachmentDatabase.THUMBNAIL, (String)null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
} catch (BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
dataFile.delete();
contentValues.put(AttachmentDatabase.DATA, (String) null);
- contentValues.put(AttachmentDatabase.THUMBNAIL, (String) null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java
index 7b605f058..ad99bb115 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java
@@ -54,7 +54,7 @@ public class BorderlessImageView extends FrameLayout {
}
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
- boolean showControls = slide.asAttachment().getDataUri() == null;
+ boolean showControls = slide.asAttachment().getUri() == null;
if (slide.hasSticker()) {
image.setFit(new CenterInside());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FullScreenDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/components/FullScreenDialogFragment.java
new file mode 100644
index 000000000..a2e68a4c8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/FullScreenDialogFragment.java
@@ -0,0 +1,51 @@
+package org.thoughtcrime.securesms.components;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.DialogFragment;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.ThemeUtil;
+
+/**
+ * Base dialog fragment for rendering as a full screen dialog with animation
+ * transitions.
+ */
+public abstract class FullScreenDialogFragment extends DialogFragment {
+
+ protected Toolbar toolbar;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
+ : R.style.TextSecure_LightTheme_FullScreenDialog);
+ }
+
+ @Override
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
+ inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
+ toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
+ toolbar.setTitle(getTitle());
+ toolbar.setNavigationOnClickListener(v -> onNavigateUp());
+ return view;
+ }
+
+ protected void onNavigateUp() {
+ dismissAllowingStateLoss();
+ }
+
+ protected abstract @StringRes int getTitle();
+
+ protected abstract @LayoutRes int getDialogLayoutResource();
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java
index d2b89606f..d1fc5341d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java
@@ -242,14 +242,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
if (!viewOnceSlides.isEmpty()) {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);
- } else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
+ } else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
thumbnailView.setVisibility(VISIBLE);
attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) {
attachmentVideoOverlayView.setVisibility(VISIBLE);
}
- glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
+ glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
.centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java b/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java
index 994f2c6b4..af89524e6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java
@@ -119,7 +119,7 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
this.activeRecipients.clear();
presentContact(contact);
- presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
+ presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
presentActionButtons(ContactUtil.getRecipients(getContext(), contact));
for (LiveRecipient recipient : activeRecipients.values()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
index b83793425..cb6f63598 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
@@ -279,7 +279,7 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().setVisibility(View.GONE);
}
- if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
+ if (slide.getUri() != null && slide.hasPlayOverlay() &&
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
{
this.playOverlay.setVisibility(View.VISIBLE);
@@ -288,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
}
if (Util.equals(slide, this.slide)) {
- Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
+ Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
return new SettableFuture<>(false);
}
if (this.slide != null && this.slide.getFastPreflightId() != null &&
- (!slide.hasVideo() || Util.equals(this.slide.getThumbnailUri(), slide.getThumbnailUri())) &&
+ (!slide.hasVideo() || Util.equals(this.slide.getUri(), slide.getUri())) &&
Util.equals(this.slide.getFastPreflightId(), slide.getFastPreflightId()))
{
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
@@ -301,7 +301,7 @@ public class ThumbnailView extends FrameLayout {
return new SettableFuture<>(false);
}
- Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri()
+ Log.i(TAG, "loading part with id " + slide.asAttachment().getUri()
+ ", progress " + slide.getTransferState() + ", fast preflight id: " +
slide.asAttachment().getFastPreflightId());
@@ -327,7 +327,7 @@ public class ThumbnailView extends FrameLayout {
blurhash.setImageDrawable(null);
}
- if (slide.getThumbnailUri() != null) {
+ if (slide.getUri() != null) {
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
SettableFuture thumbnailFuture = new SettableFuture<>();
thumbnailFuture.deferTo(result);
@@ -412,7 +412,7 @@ public class ThumbnailView extends FrameLayout {
}
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
- GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
+ GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getUri()))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(withCrossFade()), fit);
@@ -469,10 +469,10 @@ public class ThumbnailView extends FrameLayout {
private class ThumbnailClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
- if (thumbnailClickListener != null &&
- slide != null &&
- slide.asAttachment().getDataUri() != null &&
- slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
+ if (thumbnailClickListener != null &&
+ slide != null &&
+ slide.asAttachment().getUri() != null &&
+ slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
{
thumbnailClickListener.onClick(view, slide);
} else if (parentClickListener != null) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java
new file mode 100644
index 000000000..c029934ee
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java
@@ -0,0 +1,43 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.webrtc.EglBase;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+
+import java.lang.ref.WeakReference;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.WeakHashMap;
+
+public class BroadcastVideoSink implements VideoSink {
+
+ private final EglBase eglBase;
+ private final WeakHashMap sinks;
+
+ public BroadcastVideoSink(@Nullable EglBase eglBase) {
+ this.eglBase = eglBase;
+ this.sinks = new WeakHashMap<>();
+ }
+
+ public @Nullable EglBase getEglBase() {
+ return eglBase;
+ }
+
+ public void addSink(@NonNull VideoSink sink) {
+ sinks.put(sink, true);
+ }
+
+ public void removeSink(@NonNull VideoSink sink) {
+ sinks.remove(sink);
+ }
+
+ @Override
+ public void onFrame(@NonNull VideoFrame videoFrame) {
+ for (VideoSink sink : sinks.keySet()) {
+ sink.onFrame(videoFrame);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java
new file mode 100644
index 000000000..06aed83cb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java
@@ -0,0 +1,115 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
+import org.thoughtcrime.securesms.R;
+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.events.CallParticipant;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.AvatarUtil;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates views needed to show a call participant including their
+ * avatar in full screen or pip mode, and their video feed.
+ */
+public class CallParticipantView extends ConstraintLayout {
+
+ private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
+
+ private RecipientId recipientId;
+ private AvatarImageView avatar;
+ private TextureViewRenderer renderer;
+ private ImageView pipAvatar;
+ private ContactPhoto contactPhoto;
+
+ public CallParticipantView(@NonNull Context context) {
+ super(context);
+ onFinishInflate();
+ }
+
+ public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ avatar = findViewById(R.id.call_participant_item_avatar);
+ pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
+ renderer = findViewById(R.id.call_participant_renderer);
+
+ avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
+ }
+
+ void setCallParticipant(@NonNull CallParticipant participant) {
+ boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
+ recipientId = participant.getRecipient().getId();
+
+ renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
+
+ if (participant.isVideoEnabled()) {
+ if (participant.getVideoSink().getEglBase() != null) {
+ renderer.init(participant.getVideoSink().getEglBase());
+ }
+ renderer.attachBroadcastVideoSink(participant.getVideoSink());
+ } else {
+ renderer.attachBroadcastVideoSink(null);
+ }
+
+ if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
+ avatar.setAvatar(participant.getRecipient());
+ AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
+ setPipAvatar(participant.getRecipient());
+ contactPhoto = participant.getRecipient().getContactPhoto();
+ }
+ }
+
+ void setRenderInPip(boolean shouldRenderInPip) {
+ avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
+ pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
+ }
+
+ private void setPipAvatar(@NonNull Recipient recipient) {
+ ContactPhoto contactPhoto = recipient.getContactPhoto();
+ FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
+
+ GlideApp.with(this)
+ .load(contactPhoto)
+ .fallback(fallbackPhoto.asCallCard(getContext()))
+ .error(fallbackPhoto.asCallCard(getContext()))
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .into(pipAvatar);
+
+ pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
+ pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
+ }
+
+ private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
+ @Override
+ public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
+ ResourceContactPhoto photo = new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
+ photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ return photo;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java
new file mode 100644
index 000000000..51563751c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java
@@ -0,0 +1,104 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.flexbox.AlignItems;
+import com.google.android.flexbox.FlexboxLayout;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.events.CallParticipant;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Can dynamically render a collection of call participants, adjusting their
+ * sizing and layout depending on the total number of participants.
+ */
+public class CallParticipantsLayout extends FlexboxLayout {
+
+ private List callParticipants = Collections.emptyList();
+ private boolean shouldRenderInPip;
+
+ public CallParticipantsLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ void update(@NonNull List callParticipants, boolean shouldRenderInPip) {
+ this.callParticipants = callParticipants;
+ this.shouldRenderInPip = shouldRenderInPip;
+ updateLayout();
+ }
+
+ private void updateLayout() {
+ if (shouldRenderInPip && Util.hasItems(callParticipants)) {
+ updateChildrenCount(1);
+ update(0, callParticipants.get(0));
+ } else {
+ int count = callParticipants.size();
+ updateChildrenCount(count);
+
+ for (int i = 0; i < callParticipants.size(); i++) {
+ update(i, callParticipants.get(i));
+ }
+ }
+ }
+
+ private void updateChildrenCount(int count) {
+ int childCount = getChildCount();
+ if (childCount < count) {
+ for (int i = childCount; i < count; i++) {
+ addCallParticipantView();
+ }
+ } else if (childCount > count) {
+ for (int i = count; i < childCount; i++) {
+ removeViewAt(count);
+ }
+ }
+ }
+
+ private void update(int index, @NonNull CallParticipant participant) {
+ CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
+ callParticipantView.setCallParticipant(participant);
+ callParticipantView.setRenderInPip(shouldRenderInPip);
+ setChildLayoutParams(callParticipantView, index, getChildCount());
+ }
+
+ private void addCallParticipantView() {
+ View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
+ FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
+
+ params.setAlignSelf(AlignItems.STRETCH);
+ view.setLayoutParams(params);
+ addView(view);
+ }
+
+ private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
+ FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
+ if (childCount < 3) {
+ params.setFlexBasisPercent(1f);
+ } else {
+ if ((childCount % 2) != 0 && childPosition == childCount - 1) {
+ params.setFlexBasisPercent(1f);
+ } else {
+ params.setFlexBasisPercent(0.5f);
+ }
+ }
+ child.setLayoutParams(params);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java
new file mode 100644
index 000000000..0981b9dd5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java
@@ -0,0 +1,215 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.events.CallParticipant;
+import org.thoughtcrime.securesms.events.WebRtcViewModel;
+import org.thoughtcrime.securesms.ringrtc.CameraState;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the state of all participants, remote and local, combined with view state
+ * needed to properly render the participants. The view state primarily consists of
+ * if we are in System PIP mode and if we should show our video for an outgoing call.
+ */
+public final class CallParticipantsState {
+
+ private static final int SMALL_GROUP_MAX = 6;
+
+ public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
+ Collections.emptyList(),
+ CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
+ null,
+ WebRtcLocalRenderState.GONE,
+ false,
+ false,
+ false);
+
+ private final WebRtcViewModel.State callState;
+ private final List remoteParticipants;
+ private final CallParticipant localParticipant;
+ private final CallParticipant focusedParticipant;
+ private final WebRtcLocalRenderState localRenderState;
+ private final boolean isInPipMode;
+ private final boolean showVideoForOutgoing;
+ private final boolean isViewingFocusedParticipant;
+
+ public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
+ @NonNull List remoteParticipants,
+ @NonNull CallParticipant localParticipant,
+ @Nullable CallParticipant focusedParticipant,
+ @NonNull WebRtcLocalRenderState localRenderState,
+ boolean isInPipMode,
+ boolean showVideoForOutgoing,
+ boolean isViewingFocusedParticipant)
+ {
+ this.callState = callState;
+ this.remoteParticipants = remoteParticipants;
+ this.localParticipant = localParticipant;
+ this.localRenderState = localRenderState;
+ this.focusedParticipant = focusedParticipant;
+ this.isInPipMode = isInPipMode;
+ this.showVideoForOutgoing = showVideoForOutgoing;
+ this.isViewingFocusedParticipant = isViewingFocusedParticipant;
+ }
+
+ public @NonNull WebRtcViewModel.State getCallState() {
+ return callState;
+ }
+
+ public @NonNull List getGridParticipants() {
+ if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
+ return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
+ } else {
+ return getAllRemoteParticipants();
+ }
+ }
+
+ public @NonNull List getListParticipants() {
+ List listParticipants = new ArrayList<>();
+
+ if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
+ listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()));
+ } else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
+ listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
+ } else {
+ return Collections.emptyList();
+ }
+
+ listParticipants.add(CallParticipant.EMPTY);
+
+ Collections.reverse(listParticipants);
+
+ return listParticipants;
+ }
+
+ public @NonNull List getAllRemoteParticipants() {
+ return remoteParticipants;
+ }
+
+ public @NonNull CallParticipant getLocalParticipant() {
+ return localParticipant;
+ }
+
+ public @Nullable CallParticipant getFocusedParticipant() {
+ return focusedParticipant;
+ }
+
+ public @NonNull WebRtcLocalRenderState getLocalRenderState() {
+ return localRenderState;
+ }
+
+ public boolean isLargeVideoGroup() {
+ return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
+ }
+
+ public boolean isInPipMode() {
+ return isInPipMode;
+ }
+
+ public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
+ @NonNull WebRtcViewModel webRtcViewModel,
+ boolean enableVideo)
+ {
+ boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
+ if (enableVideo) {
+ newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
+ } else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
+ newShowVideoForOutgoing = false;
+ }
+
+ WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
+ oldState.isInPipMode,
+ newShowVideoForOutgoing,
+ webRtcViewModel.getState(),
+ oldState.getAllRemoteParticipants().size(),
+ oldState.isViewingFocusedParticipant);
+
+ CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
+
+ return new CallParticipantsState(webRtcViewModel.getState(),
+ webRtcViewModel.getRemoteParticipants(),
+ webRtcViewModel.getLocalParticipant(),
+ focused,
+ localRenderState,
+ oldState.isInPipMode,
+ newShowVideoForOutgoing,
+ oldState.isViewingFocusedParticipant);
+ }
+
+ public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
+ WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
+ isInPip,
+ oldState.showVideoForOutgoing,
+ oldState.callState,
+ oldState.getAllRemoteParticipants().size(),
+ oldState.isViewingFocusedParticipant);
+
+ CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
+
+ return new CallParticipantsState(oldState.callState,
+ oldState.remoteParticipants,
+ oldState.localParticipant,
+ focused,
+ localRenderState,
+ isInPip,
+ oldState.showVideoForOutgoing,
+ oldState.isViewingFocusedParticipant);
+ }
+
+ public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
+ CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
+
+ WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
+ oldState.isInPipMode,
+ oldState.showVideoForOutgoing,
+ oldState.callState,
+ oldState.getAllRemoteParticipants().size(),
+ selectedPage == SelectedPage.FOCUSED);
+
+ return new CallParticipantsState(oldState.callState,
+ oldState.remoteParticipants,
+ oldState.localParticipant,
+ focused,
+ localRenderState,
+ oldState.isInPipMode,
+ oldState.showVideoForOutgoing,
+ selectedPage == SelectedPage.FOCUSED);
+ }
+
+ private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
+ boolean isInPip,
+ boolean showVideoForOutgoing,
+ @NonNull WebRtcViewModel.State callState,
+ int numberOfRemoteParticipants,
+ boolean isViewingFocusedParticipant)
+ {
+ boolean displayLocal = !isInPip && localParticipant.isVideoEnabled();
+ WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
+
+ if (displayLocal || showVideoForOutgoing) {
+ if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
+ if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
+ localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
+ } else {
+ localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
+ }
+ } else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
+ localRenderState = WebRtcLocalRenderState.LARGE;
+ }
+ } else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
+ localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
+ }
+
+ return localRenderState;
+ }
+
+ public enum SelectedPage {
+ GRID,
+ FOCUSED
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java
index 45bbec70a..cc03f42ab 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java
@@ -9,6 +9,7 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
@@ -16,21 +17,26 @@ import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
-import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
- private static final float DECELERATION_RATE = 0.99f;
+ private static final float DECELERATION_RATE = 0.99f;
+ private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
+ private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
- private final ViewGroup parent;
- private final View child;
- private final int framePadding;
- private final int pipWidth;
- private final int pipHeight;
+ private final ViewGroup parent;
+ private final View child;
+ private final int framePadding;
+ private final Queue runAfterFling;
+ private int pipWidth;
+ private int pipHeight;
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX;
private float lastTouchY;
@@ -42,6 +48,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private double projectionY;
private VelocityTracker velocityTracker;
private int maximumFlingVelocity;
+ private boolean isLockedToBottomEnd;
+ private Interpolator interpolator;
@SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
@@ -95,6 +103,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width);
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
+ this.runAfterFling = new LinkedList<>();
+ this.interpolator = ADJUST_INTERPOLATOR;
}
public void clearVerticalBoundaries() {
@@ -105,11 +115,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
extraPaddingTop = topBoundary - parent.getTop();
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
- if (isAnimating) {
- fling();
- } else if (!isDragging) {
- onFling(null, null, 0, 0);
- }
+ adjustPip();
}
private boolean onGestureFinished(MotionEvent e) {
@@ -123,12 +129,46 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return false;
}
+ public void adjustPip() {
+ pipWidth = child.getMeasuredWidth();
+ pipHeight = child.getMeasuredHeight();
+
+ if (isAnimating) {
+ interpolator = ADJUST_INTERPOLATOR;
+
+ fling();
+ } else if (!isDragging) {
+ interpolator = ADJUST_INTERPOLATOR;
+
+ onFling(null, null, 0, 0);
+ }
+ }
+
+ public void lockToBottomEnd() {
+ isLockedToBottomEnd = true;
+ }
+
+ public void enableCorners() {
+ isLockedToBottomEnd = false;
+ }
+
+ public void performAfterFling(@NonNull Runnable runnable) {
+ if (isAnimating) {
+ runAfterFling.add(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
@Override
public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true;
+ pipWidth = child.getMeasuredWidth();
+ pipHeight = child.getMeasuredHeight();
+ interpolator = FLING_INTERPOLATOR;
return true;
}
@@ -167,6 +207,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return true;
}
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ child.performClick();
+
+ return true;
+ }
+
private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection);
@@ -178,17 +225,30 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
.translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250)
- .setInterpolator(new ViscousFluidInterpolator())
+ .setInterpolator(interpolator)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
+
+ Iterator afterFlingRunnables = runAfterFling.iterator();
+ while (afterFlingRunnables.hasNext()) {
+ Runnable runnable = afterFlingRunnables.next();
+
+ runnable.run();
+ afterFlingRunnables.remove();
+ }
}
})
.start();
}
private Point findNearestCornerPosition(Point projection) {
+ if (isLockedToBottomEnd) {
+ return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
+ : calculateBottomLeftCoordinates(parent);
+ }
+
Point maxPoint = null;
double maxDistance = Double.MAX_VALUE;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java
index b017a246a..199a81b82 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java
@@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private boolean enableFixedSize;
private int surfaceWidth;
private int surfaceHeight;
+ private boolean isInitialized;
+ private BroadcastVideoSink attachedVideoSink;
public TextureViewRenderer(@NonNull Context context) {
super(context);
@@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.setSurfaceTextureListener(this);
}
- public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) {
- this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
+ public void init(@NonNull EglBase eglBase) {
+ if (isInitialized) return;
+
+ isInitialized = true;
+
+ this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
}
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
@@ -63,6 +69,30 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
}
+ public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
+ if (attachedVideoSink == videoSink) {
+ return;
+ }
+
+ if (attachedVideoSink != null) {
+ attachedVideoSink.removeSink(this);
+ }
+
+ if (videoSink != null) {
+ videoSink.addSink(this);
+ } else {
+ clearImage();
+ }
+
+ attachedVideoSink = videoSink;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ release();
+ }
+
public void release() {
eglRenderer.release();
}
@@ -125,6 +155,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
protected void onMeasure(int widthSpec, int heightSpec) {
ThreadUtils.checkIsOnMainThread();
+ widthSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, widthSpec, 0), MeasureSpec.AT_MOST);
+ heightSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, heightSpec, 0), MeasureSpec.AT_MOST);
+
Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight);
setMeasuredDimension(size.x, size.y);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java
new file mode 100644
index 000000000..357f94ca4
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java
@@ -0,0 +1,64 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.events.CallParticipant;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+class WebRtcCallParticipantsPage {
+
+ private final List callParticipants;
+ private final boolean isSpeaker;
+ private final boolean isRenderInPip;
+
+ static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List callParticipants,
+ boolean isRenderInPip)
+ {
+ return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
+ }
+
+ static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
+ boolean isRenderInPip)
+ {
+ return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
+ }
+
+ private WebRtcCallParticipantsPage(@NonNull List callParticipants,
+ boolean isSpeaker,
+ boolean isRenderInPip)
+ {
+ this.callParticipants = callParticipants;
+ this.isSpeaker = isSpeaker;
+ this.isRenderInPip = isRenderInPip;
+ }
+
+ public @NonNull List getCallParticipants() {
+ return callParticipants;
+ }
+
+ public boolean isRenderInPip() {
+ return isRenderInPip;
+ }
+
+ public boolean isSpeaker() {
+ return isSpeaker;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
+ return isSpeaker == that.isSpeaker &&
+ isRenderInPip == that.isRenderInPip &&
+ callParticipants.equals(that.callParticipants);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(callParticipants, isSpeaker);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java
new file mode 100644
index 000000000..c53e201c6
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java
@@ -0,0 +1,127 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+
+class WebRtcCallParticipantsPagerAdapter extends ListAdapter {
+
+ private static final int VIEW_TYPE_MULTI = 0;
+ private static final int VIEW_TYPE_SINGLE = 1;
+
+ private final Runnable onPageClicked;
+
+ WebRtcCallParticipantsPagerAdapter(@NonNull Runnable onPageClicked) {
+ super(new DiffCallback());
+ this.onPageClicked = onPageClicked;
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+ }
+
+ @Override
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final ViewHolder viewHolder;
+
+ switch (viewType) {
+ case VIEW_TYPE_SINGLE:
+ viewHolder = new SingleParticipantViewHolder((CallParticipantView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.call_participant_item,
+ parent,
+ false));
+ break;
+ case VIEW_TYPE_MULTI:
+ viewHolder = new MultipleParticipantViewHolder((CallParticipantsLayout) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.webrtc_call_participants_layout,
+ parent,
+ false));
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported viewType: " + viewType);
+ }
+
+ viewHolder.itemView.setOnClickListener(unused -> onPageClicked.run());
+
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.bind(getItem(position));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return getItem(position).isSpeaker() ? VIEW_TYPE_SINGLE : VIEW_TYPE_MULTI;
+ }
+
+ static abstract class ViewHolder extends RecyclerView.ViewHolder {
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ abstract void bind(WebRtcCallParticipantsPage page);
+ }
+
+ private static class MultipleParticipantViewHolder extends ViewHolder {
+
+ private final CallParticipantsLayout callParticipantsLayout;
+
+ private MultipleParticipantViewHolder(@NonNull CallParticipantsLayout callParticipantsLayout) {
+ super(callParticipantsLayout);
+ this.callParticipantsLayout = callParticipantsLayout;
+ }
+
+ @Override
+ void bind(WebRtcCallParticipantsPage page) {
+ callParticipantsLayout.update(page.getCallParticipants(), page.isRenderInPip());
+ }
+ }
+
+ private static class SingleParticipantViewHolder extends ViewHolder {
+
+ private final CallParticipantView callParticipantView;
+
+ private SingleParticipantViewHolder(CallParticipantView callParticipantView) {
+ super(callParticipantView);
+ this.callParticipantView = callParticipantView;
+
+ ViewGroup.LayoutParams params = callParticipantView.getLayoutParams();
+
+ params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+
+ callParticipantView.setLayoutParams(params);
+ }
+
+
+ @Override
+ void bind(WebRtcCallParticipantsPage page) {
+ callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
+ callParticipantView.setRenderInPip(page.isRenderInPip());
+ }
+ }
+
+ private static final class DiffCallback extends DiffUtil.ItemCallback {
+ @Override
+ public boolean areItemsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
+ return oldItem.isSpeaker() == newItem.isSpeaker();
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
+ return oldItem.equals(newItem);
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java
new file mode 100644
index 000000000..8ba176d0e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java
@@ -0,0 +1,79 @@
+package org.thoughtcrime.securesms.components.webrtc;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.events.CallParticipant;
+
+class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter {
+
+ private static final int PARTICIPANT = 0;
+ private static final int EMPTY = 1;
+
+ protected WebRtcCallParticipantsRecyclerAdapter() {
+ super(new DiffCallback());
+ }
+
+ @Override
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == PARTICIPANT) {
+ return new ParticipantViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
+ } else {
+ return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_empty_item, parent, false));
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.bind(getItem(position));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return getItem(position) == CallParticipant.EMPTY ? EMPTY : PARTICIPANT;
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ void bind(@NonNull CallParticipant callParticipant) {}
+ }
+
+ private static class ParticipantViewHolder extends ViewHolder {
+
+ private final CallParticipantView callParticipantView;
+
+ ParticipantViewHolder(@NonNull View itemView) {
+ super(itemView);
+ callParticipantView = itemView.findViewById(R.id.call_participant);
+ }
+
+ @Override
+ void bind(@NonNull CallParticipant callParticipant) {
+ callParticipantView.setCallParticipant(callParticipant);
+ }
+ }
+
+ private static class DiffCallback extends DiffUtil.ItemCallback {
+
+ @Override
+ public boolean areItemsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
+ return oldItem.getRecipient().equals(newItem.getRecipient());
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
+ return oldItem.equals(newItem);
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java
index 30da86869..826d7a7fa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java
@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewParent;
+import android.view.animation.Animation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -13,67 +15,85 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition;
+import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
+import androidx.transition.TransitionSet;
+import androidx.viewpager2.widget.MarginPageTransformer;
+import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
-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.contacts.avatars.ProfileContactPhoto;
+import org.thoughtcrime.securesms.events.CallParticipant;
+import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState;
-import org.thoughtcrime.securesms.util.AvatarUtil;
+import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
public class WebRtcCallView extends FrameLayout {
- 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();
+ 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;
- public static final int FADE_OUT_DELAY = 5000;
+ public static final int FADE_OUT_DELAY = 5000;
+ public static final int PIP_RESIZE_DURATION = 300;
+ public static final int CONTROLS_HEIGHT = 98;
- private TextureViewRenderer localRenderer;
private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle;
- private ViewGroup largeLocalRenderContainer;
- private ViewGroup localRenderPipFrame;
- private ViewGroup smallLocalRenderContainer;
- private ViewGroup remoteRenderContainer;
+ private ViewGroup smallLocalRenderFrame;
+ private TextureViewRenderer smallLocalRender;
+ private View largeLocalRenderFrame;
+ private TextureViewRenderer largeLocalRender;
+ private View largeLocalRenderNoVideo;
+ private ImageView largeLocalRenderNoVideoAvatar;
private TextView recipientName;
private TextView status;
private ConstraintLayout parent;
- private AvatarImageView avatar;
- private ImageView avatarCard;
+ private ConstraintLayout participantsParent;
private ControlsListener controlsListener;
private RecipientId recipientId;
- private CameraState.Direction cameraDirection;
private ImageView answer;
private ImageView cameraDirectionToggle;
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
private ImageView hangup;
private View answerWithAudio;
private View answerWithAudioLabel;
- private View ongoingFooterGradient;
+ private View footerGradient;
+ private View startCallControls;
+ private ViewPager2 callParticipantsPager;
+ private RecyclerView callParticipantsRecycler;
+ private Toolbar toolbar;
+ private int pagerBottomMarginDp;
+ private boolean controlsVisible = true;
+
+ private WebRtcCallParticipantsPagerAdapter pagerAdapter;
+ private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
private final Set incomingCallViews = new HashSet<>();
private final Set topViews = new HashSet<>();
@@ -82,7 +102,8 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> {
- if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); };
+ if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
+ };
public WebRtcCallView(@NonNull Context context) {
this(context, null);
@@ -99,42 +120,61 @@ public class WebRtcCallView extends FrameLayout {
protected void onFinishInflate() {
super.onFinishInflate();
- audioToggle = findViewById(R.id.call_screen_speaker_toggle);
- videoToggle = findViewById(R.id.call_screen_video_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);
- remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder);
- recipientName = findViewById(R.id.call_screen_recipient_name);
- status = findViewById(R.id.call_screen_status);
- parent = findViewById(R.id.call_screen);
- avatar = findViewById(R.id.call_screen_recipient_avatar);
- 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);
+ audioToggle = findViewById(R.id.call_screen_speaker_toggle);
+ videoToggle = findViewById(R.id.call_screen_video_toggle);
+ micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
+ smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
+ smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
+ largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
+ largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
+ largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
+ largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
+ recipientName = findViewById(R.id.call_screen_recipient_name);
+ status = findViewById(R.id.call_screen_status);
+ parent = findViewById(R.id.call_screen);
+ participantsParent = findViewById(R.id.call_screen_participants_parent);
+ 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);
+ footerGradient = findViewById(R.id.call_screen_footer_gradient);
+ startCallControls = findViewById(R.id.call_screen_start_call_controls);
+ callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
+ callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
+ toolbar = findViewById(R.id.call_screen_toolbar);
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);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
+ View startCall = findViewById(R.id.call_screen_start_call_start_call);
+ View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
- topViews.add(status);
+ callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
+
+ pagerAdapter = new WebRtcCallParticipantsPagerAdapter(this::toggleControls);
+ recyclerAdapter = new WebRtcCallParticipantsRecyclerAdapter();
+
+ callParticipantsPager.setAdapter(pagerAdapter);
+ callParticipantsRecycler.setAdapter(recyclerAdapter);
+
+ callParticipantsPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
+ }
+ });
+
+ topViews.add(toolbar);
topViews.add(topGradient);
- topViews.add(recipientName);
incomingCallViews.add(answer);
incomingCallViews.add(answerLabel);
incomingCallViews.add(decline);
incomingCallViews.add(declineLabel);
- incomingCallViews.add(incomingFooterGradient);
+ incomingCallViews.add(footerGradient);
adjustableMarginsSet.add(micToggle);
adjustableMarginsSet.add(cameraDirectionToggle);
@@ -158,15 +198,18 @@ public class WebRtcCallView extends FrameLayout {
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
- downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
-
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
- setOnClickListener(v -> toggleControls());
- avatar.setOnClickListener(v -> toggleControls());
+ pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
- pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
+ startCall.setOnClickListener(v -> runIfNonNull(controlsListener, listener -> listener.onStartCall(videoToggle.isChecked())));
+ cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
+
+ ColorMatrix greyScaleMatrix = new ColorMatrix();
+ greyScaleMatrix.setSaturation(0);
+ largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
+ largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight);
@@ -195,67 +238,99 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false);
}
- public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
- if (isRemoteVideoEnabled) {
- remoteRenderContainer.setVisibility(View.VISIBLE);
+ public void updateCallParticipants(@NonNull CallParticipantsState state) {
+ List pages = new ArrayList<>(2);
+
+ if (!state.getGridParticipants().isEmpty()) {
+ pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
+ }
+
+ if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
+ pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
+ }
+
+ pagerAdapter.submitList(pages);
+ recyclerAdapter.submitList(state.getListParticipants());
+ updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
+
+ if (state.isLargeVideoGroup()) {
+ layoutParticipantsForLargeCount();
} else {
- remoteRenderContainer.setVisibility(View.GONE);
+ layoutParticipantsForSmallCount();
}
}
- public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) {
- if (localRenderer == surfaceViewRenderer) {
- return;
+ public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
+ smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
+ largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
+
+ smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
+ largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
+
+ if (localCallParticipant.getVideoSink().getEglBase() != null) {
+ smallLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
+ largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
}
- localRenderer = surfaceViewRenderer;
-
- if (surfaceViewRenderer == null) {
- setRenderer(largeLocalRenderContainer, null);
- setRenderer(smallLocalRenderContainer, null);
- } else {
- localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
- localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
- }
- }
-
- public void setRemoteRenderer(@Nullable TextureViewRenderer remoteRenderer) {
- setRenderer(remoteRenderContainer, remoteRenderer);
- }
-
- public void setLocalRenderState(WebRtcLocalRenderState localRenderState) {
-
- videoToggle.setChecked(localRenderState != WebRtcLocalRenderState.GONE, false);
-
- switch (localRenderState) {
+ switch (state) {
case GONE:
- localRenderPipFrame.setVisibility(View.GONE);
- largeLocalRenderContainer.setVisibility(View.GONE);
- setRenderer(largeLocalRenderContainer, null);
- setRenderer(smallLocalRenderContainer, null);
+ largeLocalRender.attachBroadcastVideoSink(null);
+ largeLocalRenderFrame.setVisibility(View.GONE);
+ smallLocalRender.attachBroadcastVideoSink(null);
+ smallLocalRenderFrame.setVisibility(View.GONE);
+
+ videoToggle.setChecked(false, false);
+ break;
+ case SMALL_RECTANGLE:
+ smallLocalRenderFrame.setVisibility(View.VISIBLE);
+ smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
+ animatePipToRectangle();
+
+ largeLocalRender.attachBroadcastVideoSink(null);
+ largeLocalRenderFrame.setVisibility(View.GONE);
+
+ videoToggle.setChecked(true, false);
+ break;
+ case SMALL_SQUARE:
+ smallLocalRenderFrame.setVisibility(View.VISIBLE);
+ smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
+ animatePipToSquare();
+
+ largeLocalRender.attachBroadcastVideoSink(null);
+ largeLocalRenderFrame.setVisibility(View.GONE);
+
+ videoToggle.setChecked(true, false);
break;
case LARGE:
- localRenderPipFrame.setVisibility(View.GONE);
- largeLocalRenderContainer.setVisibility(View.VISIBLE);
- if (largeLocalRenderContainer.getChildCount() == 0) {
- setRenderer(largeLocalRenderContainer, localRenderer);
- }
+ largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
+ largeLocalRenderFrame.setVisibility(View.VISIBLE);
+
+ largeLocalRenderNoVideo.setVisibility(View.GONE);
+ largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
+
+ smallLocalRender.attachBroadcastVideoSink(null);
+ smallLocalRenderFrame.setVisibility(View.GONE);
+
+ videoToggle.setChecked(true, false);
break;
- case SMALL:
- localRenderPipFrame.setVisibility(View.VISIBLE);
- largeLocalRenderContainer.setVisibility(View.GONE);
+ case LARGE_NO_VIDEO:
+ largeLocalRender.attachBroadcastVideoSink(null);
+ largeLocalRenderFrame.setVisibility(View.VISIBLE);
- if (smallLocalRenderContainer.getChildCount() == 0) {
- setRenderer(smallLocalRenderContainer, localRenderer);
- }
- }
- }
+ largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
+ largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
- public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
- this.cameraDirection = cameraDirection;
+ GlideApp.with(getContext().getApplicationContext())
+ .load(new ProfileContactPhoto(localCallParticipant.getRecipient(), localCallParticipant.getRecipient().getProfileAvatar()))
+ .transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .into(largeLocalRenderNoVideoAvatar);
- if (localRenderer != null) {
- localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
+ smallLocalRender.attachBroadcastVideoSink(null);
+ smallLocalRenderFrame.setVisibility(View.GONE);
+
+ videoToggle.setChecked(false, false);
+ break;
}
}
@@ -265,17 +340,16 @@ public class WebRtcCallView extends FrameLayout {
}
recipientId = recipient.getId();
- recipientName.setText(recipient.getDisplayName(getContext()));
- avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
- avatar.setAvatar(GlideApp.with(this), recipient, false);
- AvatarUtil.loadBlurredIconIntoViewBackground(recipient, this);
- setRecipientCallCard(recipient);
- }
-
- public void showCallCard(boolean showCallCard) {
- avatarCard.setVisibility(showCallCard ? VISIBLE : GONE);
- avatar.setVisibility(showCallCard ? GONE : VISIBLE);
+ if (recipient.isGroup()) {
+ recipientName.setText(R.string.WebRtcCallView__group_call);
+ if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
+ toolbar.inflateMenu(R.menu.group_call);
+ toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
+ }
+ } else {
+ recipientName.setText(recipient.getDisplayName(getContext()));
+ }
}
public void setStatus(@NonNull String status) {
@@ -302,11 +376,16 @@ public class WebRtcCallView extends FrameLayout {
}
}
- public void setWebRtcControls(WebRtcControls webRtcControls) {
+ public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
Set lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear();
+ if (webRtcControls.displayStartCallControls()) {
+ visibleViewSet.add(footerGradient);
+ visibleViewSet.add(startCallControls);
+ }
+
if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews);
}
@@ -341,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup);
- visibleViewSet.add(ongoingFooterGradient);
+ visibleViewSet.add(footerGradient);
}
if (webRtcControls.displayMuteAudio()) {
@@ -358,6 +437,12 @@ public class WebRtcCallView extends FrameLayout {
updateButtonStateForLargeButtons();
}
+ if (webRtcControls.displayRemoteVideoRecycler()) {
+ callParticipantsRecycler.setVisibility(View.VISIBLE);
+ } else {
+ callParticipantsRecycler.setVisibility(View.GONE);
+ }
+
if (webRtcControls.isFadeOutEnabled()) {
if (!controls.isFadeOutEnabled()) {
scheduleFadeOut();
@@ -378,8 +463,39 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle;
}
+ private void animatePipToRectangle() {
+ ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
+ animation.setDuration(PIP_RESIZE_DURATION);
+ animation.setAnimationListener(new SimpleAnimationListener() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ pictureInPictureGestureHelper.enableCorners();
+ pictureInPictureGestureHelper.adjustPip();
+ }
+ });
+
+ smallLocalRenderFrame.startAnimation(animation);
+ }
+
+ private void animatePipToSquare() {
+ pictureInPictureGestureHelper.lockToBottomEnd();
+
+ pictureInPictureGestureHelper.performAfterFling(() -> {
+ ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
+ animation.setDuration(PIP_RESIZE_DURATION);
+ animation.setAnimationListener(new SimpleAnimationListener() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ pictureInPictureGestureHelper.adjustPip();
+ }
+ });
+
+ smallLocalRenderFrame.startAnimation(animation);
+ });
+ }
+
private void toggleControls() {
- if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) {
+ if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
fadeOutControls();
} else {
fadeInControls();
@@ -399,9 +515,44 @@ public class WebRtcCallView extends FrameLayout {
scheduleFadeOut();
}
- private void fadeControls(int visibility) {
+ private void layoutParticipantsForSmallCount() {
+ pagerBottomMarginDp = 0;
+
+ layoutParticipants();
+ }
+
+ private void layoutParticipantsForLargeCount() {
+ pagerBottomMarginDp = 104;
+
+ layoutParticipants();
+ }
+
+ private int withControlsHeight(int margin) {
+ if (margin == 0) {
+ return 0;
+ }
+
+ return controlsVisible ? margin + CONTROLS_HEIGHT : margin;
+ }
+
+ private void layoutParticipants() {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
+ TransitionManager.beginDelayedTransition(participantsParent, transition);
+
+ ConstraintSet constraintSet = new ConstraintSet();
+ constraintSet.clone(participantsParent);
+
+ constraintSet.setMargin(R.id.call_screen_participants_pager, ConstraintSet.BOTTOM, ViewUtil.dpToPx(withControlsHeight(pagerBottomMarginDp)));
+ constraintSet.applyTo(participantsParent);
+ }
+
+ private void fadeControls(int visibility) {
+ controlsVisible = visibility == VISIBLE;
+
+ Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
+ .setDuration(TRANSITION_DURATION_MILLIS);
+
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -412,6 +563,8 @@ public class WebRtcCallView extends FrameLayout {
}
constraintSet.applyTo(parent);
+
+ layoutParticipants();
}
private void fadeInNewUiState(@NonNull Set previouslyVisibleViewSet, boolean useSmallMargins) {
@@ -458,40 +611,6 @@ public class WebRtcCallView extends FrameLayout {
}
}
- private static void setRenderer(@NonNull ViewGroup container, @Nullable View renderer) {
- if (renderer == null) {
- container.removeAllViews();
- return;
- }
-
- ViewParent parent = renderer.getParent();
- if (parent != null && parent != container) {
- ((ViewGroup) parent).removeAllViews();
- }
-
- if (parent == container) {
- return;
- }
-
- container.addView(renderer);
- }
-
- private void setRecipientCallCard(@NonNull Recipient recipient) {
- ContactPhoto contactPhoto = recipient.getContactPhoto();
- FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
-
- GlideApp.with(this).load(contactPhoto)
- .fallback(fallbackPhoto.asCallCard(getContext()))
- .error(fallbackPhoto.asCallCard(getContext()))
- .diskCacheStrategy(DiskCacheStrategy.ALL)
- .into(this.avatarCard);
-
- if (contactPhoto == null) this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- else this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_CROP);
-
- this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
- }
-
private void updateButtonStateForLargeButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
@@ -508,14 +627,14 @@ public class WebRtcCallView extends FrameLayout {
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
}
- private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
- @Override
- public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
- return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
- }
+ private boolean showParticipantsList() {
+ controlsListener.onShowParticipantsList();
+ return true;
}
public interface ControlsListener {
+ void onStartCall(boolean isVideoCall);
+ void onCancelStartCall();
void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled);
@@ -525,6 +644,7 @@ public class WebRtcCallView extends FrameLayout {
void onDenyCallPressed();
void onAcceptCallWithVoiceOnlyPressed();
void onAcceptCallPressed();
- void onDownCaretPressed();
+ void onShowParticipantsList();
+ void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java
index 81c843e29..5acc4dbdd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java
@@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
+import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel {
- private final MutableLiveData remoteVideoEnabled = new MutableLiveData<>(false);
- private final MutableLiveData microphoneEnabled = new MutableLiveData<>(true);
- private final MutableLiveData localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
- private final MutableLiveData isInPipMode = new MutableLiveData<>(false);
- private final MutableLiveData localVideoEnabled = new MutableLiveData<>(false);
- private final MutableLiveData cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
- private final LiveData shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
- private final LiveData realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
- private final MutableLiveData webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
- private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
- private final SingleLiveEvent events = new SingleLiveEvent();
- private final MutableLiveData ellapsed = new MutableLiveData<>(-1L);
- private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
-
- private boolean canDisplayTooltipIfNeeded = true;
- private boolean hasEnabledLocalVideo = false;
- private boolean showVideoForOutgoing = false;
- private long callConnectedTime = -1;
- private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
- private boolean answerWithVideoAvailable = false;
- private Runnable ellapsedTimeRunnable = this::handleTick;
+ private final MutableLiveData microphoneEnabled = new MutableLiveData<>(true);
+ private final MutableLiveData isInPipMode = new MutableLiveData<>(false);
+ private final MutableLiveData webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
+ private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
+ private final SingleLiveEvent events = new SingleLiveEvent();
+ private final MutableLiveData elapsed = new MutableLiveData<>(-1L);
+ private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
+ private final MutableLiveData participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
+ private boolean canDisplayTooltipIfNeeded = true;
+ private boolean hasEnabledLocalVideo = false;
+ private long callConnectedTime = -1;
+ private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
+ private boolean answerWithVideoAvailable = false;
+ private Runnable elapsedTimeRunnable = this::handleTick;
+ private boolean canEnterPipMode = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository();
- public LiveData getRemoteVideoEnabled() {
- return Transformations.distinctUntilChanged(remoteVideoEnabled);
- }
-
public LiveData getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
}
- public LiveData getCameraDirection() {
- return Transformations.distinctUntilChanged(cameraDirection);
- }
-
- public LiveData displaySquareCallCard() {
- return isInPipMode;
- }
-
- public LiveData getLocalRenderState() {
- return realLocalRenderState;
- }
-
public LiveData getWebRtcControls() {
return realWebRtcControls;
}
@@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
}
public LiveData getCallTime() {
- return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
+ return Transformations.map(elapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
+ }
+
+ public LiveData getCallParticipantsState() {
+ return participantsState;
+ }
+
+ public boolean canEnterPipMode() {
+ return canEnterPipMode;
}
public boolean isAnswerWithVideoAvailable() {
@@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(isInPipMode);
+
+ //noinspection ConstantConditions
+ participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
+ }
+
+ @MainThread
+ public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
+ //noinspection ConstantConditions
+ participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
}
public void onDismissedVideoTooltip() {
@@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
- remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
- microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
+ canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
- if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
- cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
- }
+ CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
- localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
+ microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
- if (enableVideo) {
- showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
- } else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
- showVideoForOutgoing = false;
- }
+ //noinspection ConstantConditions
+ participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
- updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(),
- webRtcViewModel.getLocalCameraState().isEnabled(),
+ localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(),
- webRtcViewModel.getLocalCameraState().getCameraCount() > 1,
+ localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
repository.getAudioOutput());
@@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
callConnectedTime = -1;
}
- if (webRtcViewModel.getLocalCameraState().isEnabled()) {
+ if (localParticipant.getCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false;
- hasEnabledLocalVideo = true;
+ hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
}
@@ -144,34 +132,36 @@ public class WebRtcCallViewModel extends ViewModel {
}
}
- private boolean isValidCameraDirectionForUi(CameraState.Direction direction) {
- return direction == CameraState.Direction.FRONT || direction == CameraState.Direction.BACK;
- }
-
- private void updateLocalRenderState(WebRtcViewModel.State state) {
- if (state == WebRtcViewModel.State.CALL_CONNECTED) {
- localRenderState.setValue(WebRtcLocalRenderState.SMALL);
- } else {
- localRenderState.setValue(WebRtcLocalRenderState.LARGE);
- }
- }
-
- private void updateWebRtcControls(WebRtcViewModel.State state,
+ private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
- WebRtcAudioOutput audioOutput)
+ @NonNull WebRtcAudioOutput audioOutput)
{
-
final WebRtcControls.CallState callState;
switch (state) {
+ case CALL_PRE_JOIN:
+ callState = WebRtcControls.CallState.PRE_JOIN;
+ break;
case CALL_INCOMING:
callState = WebRtcControls.CallState.INCOMING;
answerWithVideoAvailable = isRemoteVideoOffer;
break;
+ case CALL_OUTGOING:
+ case CALL_RINGING:
+ callState = WebRtcControls.CallState.OUTGOING;
+ break;
+ case CALL_ACCEPTED_ELSEWHERE:
+ case CALL_DECLINED_ELSEWHERE:
+ case CALL_ONGOING_ELSEWHERE:
+ case CALL_NEEDS_PERMISSION:
+ case CALL_BUSY:
+ case CALL_DISCONNECTED:
+ callState = WebRtcControls.CallState.ENDING;
+ break;
default:
callState = WebRtcControls.CallState.ONGOING;
}
@@ -180,25 +170,19 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
- isInPipMode.getValue() == Boolean.TRUE,
+ Boolean.TRUE.equals(isInPipMode.getValue()),
callState,
audioOutput));
}
- private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
- if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
- else return WebRtcLocalRenderState.GONE;
- }
-
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
- if (isInPipMode) return WebRtcControls.PIP;
- else return controls;
+ return isInPipMode ? WebRtcControls.PIP : controls;
}
private void startTimer() {
cancelTimer();
- ellapsedTimeHandler.post(ellapsedTimeRunnable);
+ elapsedTimeHandler.post(elapsedTimeRunnable);
}
private void handleTick() {
@@ -208,13 +192,13 @@ public class WebRtcCallViewModel extends ViewModel {
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
- ellapsed.postValue(newValue);
+ elapsed.postValue(newValue);
- ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000);
+ elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
}
private void cancelTimer() {
- ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable);
+ elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java
index 4472a00c4..7f3c252a6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java
@@ -36,24 +36,32 @@ public final class WebRtcControls {
this.audioOutput = audioOutput;
}
+ boolean displayStartCallControls() {
+ return isPreJoin();
+ }
+
boolean displayEndCall() {
- return isOngoing();
+ return isAtLeastOutgoing();
}
boolean displayMuteAudio() {
- return isOngoing();
+ return isPreJoin() || isAtLeastOutgoing();
}
boolean displayVideoToggle() {
- return isOngoing();
+ return isPreJoin() || isAtLeastOutgoing();
}
boolean displayAudioToggle() {
- return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
+ return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
}
boolean displayCameraToggle() {
- return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
+ return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
+ }
+
+ boolean displayRemoteVideoRecycler() {
+ return isOngoing();
}
boolean displayAnswerWithAudio() {
@@ -73,25 +81,29 @@ public final class WebRtcControls {
}
boolean isFadeOutEnabled() {
- return isOngoing() && isRemoteVideoEnabled;
+ return isAtLeastOutgoing() && isRemoteVideoEnabled;
}
boolean displaySmallOngoingCallButtons() {
- return isOngoing() && displayAudioToggle() && displayCameraToggle();
+ return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle();
}
boolean displayLargeOngoingCallButtons() {
- return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
+ return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle());
}
boolean displayTopViews() {
return !isInPipMode;
}
- WebRtcAudioOutput getAudioOutput() {
+ @NonNull WebRtcAudioOutput getAudioOutput() {
return audioOutput;
}
+ private boolean isPreJoin() {
+ return callState == CallState.PRE_JOIN;
+ }
+
private boolean isOngoing() {
return callState == CallState.ONGOING;
}
@@ -100,9 +112,20 @@ public final class WebRtcControls {
return callState == CallState.INCOMING;
}
+ private boolean isAtLeastOutgoing() {
+ return callState.isAtLeast(CallState.OUTGOING);
+ }
+
public enum CallState {
NONE,
+ PRE_JOIN,
INCOMING,
- ONGOING
+ OUTGOING,
+ ONGOING,
+ ENDING;
+
+ boolean isAtLeast(@NonNull CallState other) {
+ return compareTo(other) >= 0;
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java
index 6e4873b6d..60fa4494d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState {
GONE,
- SMALL,
- LARGE
+ SMALL_RECTANGLE,
+ SMALL_SQUARE,
+ LARGE,
+ LARGE_NO_VIDEO
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java
new file mode 100644
index 000000000..bb792fb57
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java
@@ -0,0 +1,18 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
+
+public class CallParticipantViewHolder extends RecipientViewHolder {
+ public CallParticipantViewHolder(@NonNull View itemView) {
+ super(itemView, null);
+ }
+
+ @Override
+ public void bind(@NonNull CallParticipantViewState model) {
+ super.bind(model);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewState.java
new file mode 100644
index 000000000..579c31b91
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewState.java
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.events.CallParticipant;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
+
+public final class CallParticipantViewState extends RecipientMappingModel {
+
+ private final CallParticipant callParticipant;
+
+ CallParticipantViewState(@NonNull CallParticipant callParticipant) {
+ this.callParticipant = callParticipant;
+ }
+
+ @Override
+ public @NonNull Recipient getRecipient() {
+ return callParticipant.getRecipient();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListAdapter.java
new file mode 100644
index 000000000..33002292b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListAdapter.java
@@ -0,0 +1,13 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingAdapter;
+
+public class CallParticipantsListAdapter extends MappingAdapter {
+
+ CallParticipantsListAdapter() {
+ registerFactory(CallParticipantsListHeader.class, new LayoutFactory<>(CallParticipantsListHeaderViewHolder::new, R.layout.call_participants_list_header));
+ registerFactory(CallParticipantViewState.class, new LayoutFactory<>(CallParticipantViewHolder::new, R.layout.call_participants_list_item));
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java
new file mode 100644
index 000000000..0d6a7b96a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java
@@ -0,0 +1,92 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import android.os.Bundle;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
+import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
+import org.thoughtcrime.securesms.events.CallParticipant;
+import org.thoughtcrime.securesms.util.BottomSheetUtil;
+import org.thoughtcrime.securesms.util.MappingModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CallParticipantsListDialog extends BottomSheetDialogFragment {
+
+ private RecyclerView participantList;
+ private CallParticipantsListAdapter adapter;
+
+ public static void show(@NonNull FragmentManager manager) {
+ CallParticipantsListDialog fragment = new CallParticipantsListDialog();
+
+ fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
+ }
+
+ @Override
+ public void show(@NonNull FragmentManager manager, @Nullable String tag) {
+ BottomSheetUtil.show(manager, tag, this);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme);
+ LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper);
+
+ participantList = (RecyclerView) themedInflater.inflate(R.layout.call_participants_list_dialog, container, false);
+
+ return participantList;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
+
+ initializeList();
+
+ viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList);
+ }
+
+ private void initializeList() {
+ adapter = new CallParticipantsListAdapter();
+
+ participantList.setLayoutManager(new LinearLayoutManager(requireContext()));
+ participantList.setAdapter(adapter);
+ }
+
+ private void updateList(@NonNull CallParticipantsState callParticipantsState) {
+ List> items = new ArrayList<>();
+
+ items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
+
+ items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
+ for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
+ items.add(new CallParticipantViewState(callParticipant));
+ }
+
+ adapter.submitList(items);
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeader.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeader.java
new file mode 100644
index 000000000..5e46b4469
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeader.java
@@ -0,0 +1,31 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingModel;
+
+public class CallParticipantsListHeader implements MappingModel {
+
+ private int participantCount;
+
+ public CallParticipantsListHeader(int participantCount) {
+ this.participantCount = participantCount;
+ }
+
+ @NonNull String getHeader(@NonNull Context context) {
+ return context.getResources().getQuantityString(R.plurals.CallParticipantsListDialog_in_this_call_d_people, participantCount, participantCount);
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull CallParticipantsListHeader newItem) {
+ return true;
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull CallParticipantsListHeader newItem) {
+ return participantCount == newItem.participantCount;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeaderViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeaderViewHolder.java
new file mode 100644
index 000000000..3d5ec7c04
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListHeaderViewHolder.java
@@ -0,0 +1,24 @@
+package org.thoughtcrime.securesms.components.webrtc.participantslist;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingViewHolder;
+
+public class CallParticipantsListHeaderViewHolder extends MappingViewHolder {
+
+ private final TextView headerText;
+
+ public CallParticipantsListHeaderViewHolder(@NonNull View itemView) {
+ super(itemView);
+ headerText = findViewById(R.id.call_participants_list_header);
+ }
+
+ @Override
+ public void bind(@NonNull CallParticipantsListHeader model) {
+ headerText.setText(model.getHeader(getContext()));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java
index 1d69397aa..68ba94386 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java
@@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable;
@@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
private final int smallResourceId;
private final int callCardResourceId;
+ private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
+
public ResourceContactPhoto(@DrawableRes int resourceId) {
this(resourceId, resourceId, resourceId);
}
@@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
this.smallResourceId = smallResourceId;
}
+ public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
+ this.scaleType = scaleType;
+ }
+
@Override
- public Drawable asDrawable(Context context, int color) {
+ public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
return asDrawable(context, color, false);
}
@Override
- public Drawable asDrawable(Context context, int color, boolean inverted) {
+ public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, resourceId, color, inverted);
}
@Override
- public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
+ public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, smallResourceId, color, inverted);
}
- private Drawable buildDrawable(Context context, int resourceId, int color, boolean inverted) {
+ private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
- foreground.setScaleType(ImageView.ScaleType.CENTER);
+ //noinspection ConstantConditions
+ foreground.setScaleType(scaleType);
if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
@@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
}
@Override
- public Drawable asCallCard(Context context) {
+ public @Nullable Drawable asCallCard(@NonNull Context context) {
return AppCompatResources.getDrawable(context, callCardResourceId);
}
private static class ExpandingLayerDrawable extends LayerDrawable {
- public ExpandingLayerDrawable(Drawable[] layers) {
+ public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
super(layers);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV1.java
deleted file mode 100644
index 7f6bb47e1..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV1.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.thoughtcrime.securesms.contacts.sync;
-
-import androidx.annotation.NonNull;
-
-import com.annimon.stream.Collectors;
-import com.annimon.stream.Stream;
-
-import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.util.SetUtil;
-import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
-import org.whispersystems.signalservice.api.push.ContactTokenDetails;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-
-class ContactDiscoveryV1 {
-
- private static final String TAG = ContactDiscoveryV1.class.getSimpleName();
-
- static @NonNull DirectoryResult getDirectoryResult(@NonNull Set databaseNumbers,
- @NonNull Set systemNumbers)
- throws IOException
- {
- Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
- FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
- List activeTokens = getTokens(inputResult.getNumbers());
- Set activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
- FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
- HashMap uuids = new HashMap<>();
-
- for (String number : outputResult.getNumbers()) {
- uuids.put(number, null);
- }
-
- return new DirectoryResult(uuids, outputResult.getRewrites());
- }
-
- static @NonNull DirectoryResult getDirectoryResult(@NonNull String number) throws IOException {
- return getDirectoryResult(Collections.singleton(number), Collections.singleton(number));
- }
-
- private static @NonNull List getTokens(@NonNull Set numbers) throws IOException {
- SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
-
- if (numbers.size() == 1) {
- Optional details = accountManager.getContact(numbers.iterator().next());
- return details.isPresent() ? Collections.singletonList(details.get()) : Collections.emptyList();
- } else {
- return accountManager.getContacts(numbers);
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java
index fdc6b3aab..4e17def4b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java
@@ -31,6 +31,9 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
+/**
+ * Uses CDS to map E164's to UUIDs.
+ */
class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java
index 8cd3ab718..05135fd6c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java
@@ -25,20 +25,21 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
+import org.thoughtcrime.securesms.database.SessionDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
+import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
-import org.thoughtcrime.securesms.recipients.RecipientDetails;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -50,10 +51,13 @@ import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
+import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.Calendar;
@@ -154,13 +158,7 @@ public class DirectoryHelper {
return RegisteredState.NOT_REGISTERED;
}
- DirectoryResult result;
-
- if (FeatureFlags.cds()) {
- result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
- } else {
- result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
- }
+ DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
stopwatch.split("e164-network");
@@ -179,6 +177,13 @@ public class DirectoryHelper {
} else {
recipientDatabase.markRegistered(recipient.getId());
}
+ } else if (recipient.hasUuid() && recipient.isRegistered() && hasCommunicatedWith(context, recipient)) {
+ if (isUuidRegistered(context, recipient)) {
+ recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
+ } else {
+ recipientDatabase.markUnregistered(recipient.getId());
+ }
+ stopwatch.split("e164-unlisted-network");
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
@@ -218,13 +223,7 @@ public class DirectoryHelper {
Stopwatch stopwatch = new Stopwatch("refresh");
- DirectoryResult result;
-
- if (FeatureFlags.cds()) {
- result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
- } else {
- result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
- }
+ DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
stopwatch.split("network");
@@ -244,6 +243,17 @@ public class DirectoryHelper {
stopwatch.split("process-cds");
+ UnlistedResult unlistedResult = filterForUnlistedUsers(context, inactiveIds);
+
+ inactiveIds.removeAll(unlistedResult.getPossiblyActive());
+
+ if (unlistedResult.getRetries().size() > 0) {
+ Log.i(TAG, "Some profile fetches failed to resolve. Assuming not-inactive for now and scheduling a retry.");
+ RetrieveProfileJob.enqueue(unlistedResult.getRetries());
+ }
+
+ stopwatch.split("handle-unlisted");
+
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
stopwatch.split("update-registered");
@@ -275,16 +285,10 @@ public class DirectoryHelper {
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
- ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
+ ProfileUtil.retrieveProfileSync(context, recipient, SignalServiceProfile.RequestType.PROFILE);
return true;
- } catch (ExecutionException e) {
- if (e.getCause() instanceof NotFoundException) {
- return false;
- } else {
- throw new IOException(e);
- }
- } catch (InterruptedException | TimeoutException e) {
- throw new IOException(e);
+ } catch (NotFoundException e) {
+ return false;
}
}
@@ -420,6 +424,50 @@ public class DirectoryHelper {
}).collect(Collectors.toSet());
}
+ /**
+ * Users can mark themselves as 'unlisted' in CDS, meaning that even if CDS says they're
+ * unregistered, they might actually be registered. We need to double-check users who we already
+ * have UUIDs for. Also, we only want to bother doing this for users we have conversations for,
+ * so we will also only check for users that have a thread.
+ */
+ private static UnlistedResult filterForUnlistedUsers(@NonNull Context context, @NonNull Set inactiveIds) {
+ List possiblyUnlisted = Stream.of(inactiveIds)
+ .map(Recipient::resolved)
+ .filter(Recipient::isRegistered)
+ .filter(Recipient::hasUuid)
+ .filter(r -> hasCommunicatedWith(context, r))
+ .toList();
+
+ List>> futures = Stream.of(possiblyUnlisted)
+ .map(r -> new Pair<>(r, ProfileUtil.retrieveProfile(context, r, SignalServiceProfile.RequestType.PROFILE)))
+ .toList();
+ Set potentiallyActiveIds = new HashSet<>();
+ Set retries = new HashSet<>();
+
+ Stream.of(futures)
+ .forEach(pair -> {
+ try {
+ pair.second().get(5, TimeUnit.SECONDS);
+ potentiallyActiveIds.add(pair.first().getId());
+ } catch (InterruptedException | TimeoutException e) {
+ retries.add(pair.first().getId());
+ potentiallyActiveIds.add(pair.first().getId());
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof NotFoundException)) {
+ retries.add(pair.first().getId());
+ potentiallyActiveIds.add(pair.first().getId());
+ }
+ }
+ });
+
+ return new UnlistedResult(potentiallyActiveIds, retries);
+ }
+
+ private static boolean hasCommunicatedWith(@NonNull Context context, @NonNull Recipient recipient) {
+ return DatabaseFactory.getThreadDatabase(context).hasThread(recipient.getId()) ||
+ DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.getId());
+ }
+
static class DirectoryResult {
private final Map registeredNumbers;
private final Map numberRewrites;
@@ -441,6 +489,24 @@ public class DirectoryHelper {
}
}
+ private static class UnlistedResult {
+ private final Set possiblyActive;
+ private final Set retries;
+
+ private UnlistedResult(@NonNull Set possiblyActive, @NonNull Set retries) {
+ this.possiblyActive = possiblyActive;
+ this.retries = retries;
+ }
+
+ @NonNull Set getPossiblyActive() {
+ return possiblyActive;
+ }
+
+ @NonNull Set getRetries() {
+ return retries;
+ }
+ }
+
private static class AccountHolder {
private final boolean fresh;
private final Account account;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java
index 54b06a096..2a9b970e2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/Contact.java
@@ -14,7 +14,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.UriAttachment;
-import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -648,7 +647,7 @@ public class Contact implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(attachment != null ? attachment.getDataUri() : null, flags);
+ dest.writeParcelable(attachment != null ? attachment.getUri() : null, flags);
dest.writeByte((byte) (isProfile ? 1 : 0));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactFieldAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactFieldAdapter.java
index 8805f7bfd..727392ee3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactFieldAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactFieldAdapter.java
@@ -215,7 +215,7 @@ class ContactFieldAdapter extends RecyclerView.Adapter valuesArray = new ArrayList<>(1);
valuesArray.add(values);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java
index cbd66dd12..2d8298b22 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java
@@ -96,7 +96,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
presentContact(contact);
presentActionButtons(ContactUtil.getRecipients(this, contact));
- presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
+ presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
for (LiveRecipient recipient : activeRecipients.values()) {
recipient.observe(this, r -> presentActionButtons(Collections.singletonList(r.getId())));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
index bd8d2c816..6b829e10a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
@@ -93,66 +93,7 @@ public class SharedContactRepository {
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
VCard vcard = Ezvcard.parse(stream).first();
-
- ezvcard.property.StructuredName vName = vcard.getStructuredName();
- List vPhones = vcard.getTelephoneNumbers();
- List vEmails = vcard.getEmails();
- List vPostalAddresses = vcard.getAddresses();
-
- String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
- String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
-
- if (displayName == null && vName != null) {
- displayName = vName.getGiven();
- }
-
- if (displayName == null && vcard.getOrganization() != null) {
- displayName = organization;
- }
-
- if (displayName == null) {
- throw new IOException("No valid name.");
- }
-
- Name name = new Name(displayName,
- vName != null ? vName.getGiven() : null,
- vName != null ? vName.getFamily() : null,
- vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
- vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
- null);
-
-
- List phoneNumbers = new ArrayList<>(vPhones.size());
- for (ezvcard.property.Telephone vEmail : vPhones) {
- String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
-
- // Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
- String phoneNumberFromText = vEmail.getText();
- String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
- phoneNumbers.add(new Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
- }
-
- List emails = new ArrayList<>(vEmails.size());
- for (ezvcard.property.Email vEmail : vEmails) {
- String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
- emails.add(new Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
- }
-
- List postalAddresses = new ArrayList<>(vPostalAddresses.size());
- for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
- String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
- postalAddresses.add(new PostalAddress(postalAddressTypeFromVcardType(label),
- label,
- vPostalAddress.getStreetAddress(),
- vPostalAddress.getPoBox(),
- null,
- vPostalAddress.getLocality(),
- vPostalAddress.getRegion(),
- vPostalAddress.getPostalCode(),
- vPostalAddress.getCountry()));
- }
-
- contact = new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
+ contact = VCardUtil.getContactFromVcard(vcard);
} catch (IOException e) {
Log.w(TAG, "Failed to parse the vcard.", e);
}
@@ -201,7 +142,7 @@ public class SharedContactRepository {
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
Phone existing = numberMap.get(number);
- Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel);
+ Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(cursorType), cursorLabel);
if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
numberMap.put(number, candidate);
@@ -224,7 +165,7 @@ public class SharedContactRepository {
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
- emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel));
+ emails.add(new Email(cursorEmail, VCardUtil.emailTypeFromContactType(cursorType), cursorLabel));
}
}
@@ -247,7 +188,7 @@ public class SharedContactRepository {
String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
- postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType),
+ postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType),
cursorLabel,
cursorStreet,
cursorPoBox,
@@ -304,70 +245,6 @@ public class SharedContactRepository {
return null;
}
- private Phone.Type phoneTypeFromContactType(int type) {
- switch (type) {
- case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
- return Phone.Type.HOME;
- case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
- return Phone.Type.MOBILE;
- case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
- return Phone.Type.WORK;
- }
- return Phone.Type.CUSTOM;
- }
-
- private Phone.Type phoneTypeFromVcardType(@Nullable String type) {
- if ("home".equalsIgnoreCase(type)) return Phone.Type.HOME;
- else if ("cell".equalsIgnoreCase(type)) return Phone.Type.MOBILE;
- else if ("work".equalsIgnoreCase(type)) return Phone.Type.WORK;
- else return Phone.Type.CUSTOM;
- }
-
- private Email.Type emailTypeFromContactType(int type) {
- switch (type) {
- case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
- return Email.Type.HOME;
- case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
- return Email.Type.MOBILE;
- case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
- return Email.Type.WORK;
- }
- return Email.Type.CUSTOM;
- }
-
- private Email.Type emailTypeFromVcardType(@Nullable String type) {
- if ("home".equalsIgnoreCase(type)) return Email.Type.HOME;
- else if ("cell".equalsIgnoreCase(type)) return Email.Type.MOBILE;
- else if ("work".equalsIgnoreCase(type)) return Email.Type.WORK;
- else return Email.Type.CUSTOM;
- }
-
- private PostalAddress.Type postalAddressTypeFromContactType(int type) {
- switch (type) {
- case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
- return PostalAddress.Type.HOME;
- case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
- return PostalAddress.Type.WORK;
- }
- return PostalAddress.Type.CUSTOM;
- }
-
- private PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
- if ("home".equalsIgnoreCase(type)) return PostalAddress.Type.HOME;
- else if ("work".equalsIgnoreCase(type)) return PostalAddress.Type.WORK;
- else return PostalAddress.Type.CUSTOM;
- }
-
- private String getCleanedVcardType(@Nullable String type) {
- if (TextUtils.isEmpty(type)) return "";
-
- if (type.startsWith("x-") && type.length() > 2) {
- return type.substring(2);
- }
-
- return type;
- }
-
interface ValueCallback {
void onComplete(@NonNull T value);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java
new file mode 100644
index 000000000..e4400080d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java
@@ -0,0 +1,160 @@
+package org.thoughtcrime.securesms.contactshare;
+
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.logging.Log;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import ezvcard.Ezvcard;
+import ezvcard.VCard;
+
+public final class VCardUtil {
+
+ private VCardUtil(){}
+
+ private static final String TAG = VCardUtil.class.getSimpleName();
+
+ public static List parseContacts(@NonNull String vCardData) {
+ List vContacts = Ezvcard.parse(vCardData).all();
+ List contacts = new LinkedList<>();
+ for (VCard vCard: vContacts){
+ contacts.add(getContactFromVcard(vCard));
+ }
+ return contacts;
+ }
+
+ static @Nullable Contact getContactFromVcard(@NonNull VCard vcard) {
+ ezvcard.property.StructuredName vName = vcard.getStructuredName();
+ List vPhones = vcard.getTelephoneNumbers();
+ List vEmails = vcard.getEmails();
+ List vPostalAddresses = vcard.getAddresses();
+
+ String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
+ String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
+
+ if (displayName == null && vName != null) {
+ displayName = vName.getGiven();
+ }
+
+ if (displayName == null && vcard.getOrganization() != null) {
+ displayName = organization;
+ }
+
+ if (displayName == null) {
+ Log.w(TAG, "Failed to parse the vcard: No valid name.");
+ return null;
+ }
+
+ Contact.Name name = new Contact.Name(displayName,
+ vName != null ? vName.getGiven() : null,
+ vName != null ? vName.getFamily() : null,
+ vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
+ vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
+ null);
+
+
+ List phoneNumbers = new ArrayList<>(vPhones.size());
+ for (ezvcard.property.Telephone vEmail : vPhones) {
+ String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
+
+ // Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
+ String phoneNumberFromText = vEmail.getText();
+ String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
+ phoneNumbers.add(new Contact.Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
+ }
+
+ List emails = new ArrayList<>(vEmails.size());
+ for (ezvcard.property.Email vEmail : vEmails) {
+ String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
+ emails.add(new Contact.Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
+ }
+
+ List postalAddresses = new ArrayList<>(vPostalAddresses.size());
+ for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
+ String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
+ postalAddresses.add(new Contact.PostalAddress(postalAddressTypeFromVcardType(label),
+ label,
+ vPostalAddress.getStreetAddress(),
+ vPostalAddress.getPoBox(),
+ null,
+ vPostalAddress.getLocality(),
+ vPostalAddress.getRegion(),
+ vPostalAddress.getPostalCode(),
+ vPostalAddress.getCountry()));
+ }
+
+ return new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
+ }
+
+ static Contact.Phone.Type phoneTypeFromContactType(int type) {
+ switch (type) {
+ case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
+ return Contact.Phone.Type.HOME;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
+ return Contact.Phone.Type.MOBILE;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
+ return Contact.Phone.Type.WORK;
+ }
+ return Contact.Phone.Type.CUSTOM;
+ }
+
+ private static Contact.Phone.Type phoneTypeFromVcardType(@Nullable String type) {
+ if ("home".equalsIgnoreCase(type)) return Contact.Phone.Type.HOME;
+ else if ("cell".equalsIgnoreCase(type)) return Contact.Phone.Type.MOBILE;
+ else if ("work".equalsIgnoreCase(type)) return Contact.Phone.Type.WORK;
+ else return Contact.Phone.Type.CUSTOM;
+ }
+
+ static Contact.Email.Type emailTypeFromContactType(int type) {
+ switch (type) {
+ case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
+ return Contact.Email.Type.HOME;
+ case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
+ return Contact.Email.Type.MOBILE;
+ case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
+ return Contact.Email.Type.WORK;
+ }
+ return Contact.Email.Type.CUSTOM;
+ }
+
+ private static Contact.Email.Type emailTypeFromVcardType(@Nullable String type) {
+ if ("home".equalsIgnoreCase(type)) return Contact.Email.Type.HOME;
+ else if ("cell".equalsIgnoreCase(type)) return Contact.Email.Type.MOBILE;
+ else if ("work".equalsIgnoreCase(type)) return Contact.Email.Type.WORK;
+ else return Contact.Email.Type.CUSTOM;
+ }
+
+ static Contact.PostalAddress.Type postalAddressTypeFromContactType(int type) {
+ switch (type) {
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
+ return Contact.PostalAddress.Type.HOME;
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
+ return Contact.PostalAddress.Type.WORK;
+ }
+ return Contact.PostalAddress.Type.CUSTOM;
+ }
+
+ private static Contact.PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
+ if ("home".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.HOME;
+ else if ("work".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.WORK;
+ else return Contact.PostalAddress.Type.CUSTOM;
+ }
+
+ private static String getCleanedVcardType(@Nullable String type) {
+ if (TextUtils.isEmpty(type)) return "";
+
+ if (type.startsWith("x-") && type.length() > 2) {
+ return type.substring(2);
+ }
+
+ return type;
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index f59a7aa03..2a4e27584 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -676,11 +676,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
for (Media mediaItem : result.getNonUploadedMedia()) {
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
- slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
+ slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
- slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
+ slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
- slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
+ slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
} else {
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
}
@@ -1913,13 +1913,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (previewState == null) return;
if (previewState.isLoading()) {
- Log.d(TAG, "Loading link preview.");
inputPanel.setLinkPreviewLoading();
} else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
- Log.d(TAG, "No preview found.");
inputPanel.setLinkPreviewNoPreview(previewState.getError());
} else {
- Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
index e33f1d57f..bad8da4f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
@@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
@@ -84,6 +85,7 @@ public class ConversationAdapter
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private final ItemClickListener clickListener;
+ private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests;
private final Locale locale;
private final Recipient recipient;
@@ -99,12 +101,14 @@ public class ConversationAdapter
private View headerView;
private View footerView;
- ConversationAdapter(@NonNull GlideRequests glideRequests,
+ ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@NonNull Recipient recipient)
{
super(new DiffCallback());
+ this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.locale = locale;
@@ -170,8 +174,6 @@ public class ConversationAdapter
case MESSAGE_TYPE_OUTGOING_TEXT:
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
case MESSAGE_TYPE_UPDATE:
- long start = System.currentTimeMillis();
-
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
BindableConversationItem bindable = (BindableConversationItem) itemView;
@@ -190,7 +192,6 @@ public class ConversationAdapter
bindable.setEventListener(clickListener);
- 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());
@@ -219,7 +220,8 @@ public class ConversationAdapter
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
- conversationViewHolder.getBindable().bind(conversationMessage,
+ conversationViewHolder.getBindable().bind(lifecycleOwner,
+ conversationMessage,
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
glideRequests,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 1723288e3..6c710cdc9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -479,7 +479,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId());
- ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
+ ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
list.setAdapter(adapter);
setStickyHeaderDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool());
@@ -810,7 +810,7 @@ public class ConversationFragment extends LoggingFragment {
.toList();
for (Attachment attachment : attachments) {
- Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
+ Uri uri = attachment.getUri();
if (uri != null) {
mediaList.add(new Media(uri,
@@ -1424,7 +1424,10 @@ public class ConversationFragment extends LoggingFragment {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
@Nullable ScrollRequestValidator scrollRequestValidator)
{
- super(recyclerView, scrollRequestValidator);
+ super(recyclerView, scrollRequestValidator, () -> {
+ list.scrollToPosition(0);
+ list.post(ConversationFragment.this::postMarkAsReadRequest);
+ });
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
index 66d90f6d2..714334c83 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -54,6 +54,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
import com.annimon.stream.Stream;
@@ -79,8 +80,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
-import org.thoughtcrime.securesms.database.MmsDatabase;
-import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -116,9 +115,9 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
+import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
-import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
@Override
- public void bind(@NonNull ConversationMessage conversationMessage,
+ public void bind(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull ConversationMessage conversationMessage,
@NonNull Optional previousMessageRecord,
@NonNull Optional nextMessageRecord,
@NonNull GlideRequests glideRequests,
@@ -336,6 +336,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
}
+ if (hasSharedContact(messageRecord)) {
+ int contactWidth = sharedContactStub.get().getMeasuredWidth();
+ int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get());
+
+ if (contactWidth != availableWidth) {
+ sharedContactStub.get().getLayoutParams().width = availableWidth;
+ needsMeasure = true;
+ }
+ }
+
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
@@ -892,12 +902,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) {
- if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
- sharedContactStub.get().setSingularStyle();
- } else if (current.isOutgoing()) {
- sharedContactStub.get().setClusteredOutgoingStyle();
- } else {
- sharedContactStub.get().setClusteredIncomingStyle();
+ if (TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))){
+ if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
+ sharedContactStub.get().setSingularStyle();
+ } else if (current.isOutgoing()) {
+ sharedContactStub.get().setClusteredOutgoingStyle();
+ } else {
+ sharedContactStub.get().setClusteredIncomingStyle();
+ }
}
}
@@ -1075,7 +1087,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
return stickerFooter;
- } else if (hasSharedContact(messageRecord)) {
+ } else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return sharedContactStub.get().getFooter();
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return mediaThumbnailStub.get().getFooter();
@@ -1442,7 +1454,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
Log.i(TAG, "Public URI: " + publicUri);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
+ intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), Intent.normalizeMimeType(slide.getContentType()));
try {
context.startActivity(intent);
} catch (ActivityNotFoundException anfe) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
index e128b3985..76ae6f78b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
@@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
@@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.DateUtils;
-import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
@@ -44,9 +44,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout
- implements RecipientForeverObserver,
- BindableConversationItem,
- Observer
+ implements BindableConversationItem
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout
private Locale locale;
private LiveData displayBody;
- private final Debouncer bodyClearDebouncer = new Debouncer(150);
+ private final UpdateObserver updateObserver = new UpdateObserver();
+ private final SenderObserver senderObserver = new SenderObserver();
public ConversationUpdateItem(Context context) {
super(context);
@@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout
}
@Override
- public void bind(@NonNull ConversationMessage conversationMessage,
+ public void bind(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull ConversationMessage conversationMessage,
@NonNull Optional previousMessageRecord,
@NonNull Optional nextMessageRecord,
@NonNull GlideRequests glideRequests,
@@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout
{
this.batchSelected = batchSelected;
- bind(conversationMessage, locale);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- unbind();
- super.onDetachedFromWindow();
+ bind(lifecycleOwner, conversationMessage, locale);
}
@Override
@@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout
return conversationMessage;
}
- private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
- if (this.sender != null) {
- this.sender.removeForeverObserver(this);
- }
-
- observeDisplayBody(null);
- setBodyText(null);
-
+ private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
- this.sender = messageRecord.getIndividualRecipient().live();
this.locale = locale;
- this.sender.observeForever(this);
+ observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
- LiveData spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new);
+ LiveData spannableStringMessage = toSpannable(loading(liveUpdateMessage));
present(conversationMessage);
- observeDisplayBody(spannableStringMessage);
+ observeDisplayBody(lifecycleOwner, spannableStringMessage);
}
- private void observeDisplayBody(@Nullable LiveData displayBody) {
+ /** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
+ private @NonNull LiveData loading(@NonNull LiveData string) {
+ return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
+ }
+
+ private static LiveData toSpannable(LiveData loading) {
+ return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
+ }
+
+ @Override
+ public void unbind() {
+ }
+
+ private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
+ if (sender != null) {
+ sender.getLiveData().removeObserver(senderObserver);
+ }
+
+ if (recipient != null) {
+ sender = recipient.live();
+ sender.getLiveData().observe(lifecycleOwner, senderObserver);
+ } else {
+ sender = null;
+ }
+ }
+
+ private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData displayBody) {
if (this.displayBody != displayBody) {
if (this.displayBody != null) {
- this.displayBody.removeObserver(this);
+ this.displayBody.removeObserver(updateObserver);
}
this.displayBody = displayBody;
if (this.displayBody != null) {
- this.displayBody.observeForever(this);
+ this.displayBody.observe(lifecycleOwner, updateObserver);
}
}
}
private void setBodyText(@Nullable CharSequence text) {
if (text == null) {
- bodyClearDebouncer.publish(() -> body.setText(null));
+ body.setVisibility(INVISIBLE);
} else {
- bodyClearDebouncer.clear();
body.setText(text);
body.setVisibility(VISIBLE);
}
@@ -186,7 +197,7 @@ public final class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
- date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
+ date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateSent()));
title.setVisibility(GONE);
date.setVisibility(View.VISIBLE);
@@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout
icon.setColorFilter(getIconTintFilter());
}
- @Override
- public void onRecipientChanged(@NonNull Recipient recipient) {
- present(conversationMessage);
- }
-
@Override
public void setOnClickListener(View.OnClickListener l) {
super.setOnClickListener(new InternalClickListener(l));
}
- @Override
- public void unbind() {
- if (sender != null) {
- sender.removeForeverObserver(this);
- }
+ private final class SenderObserver implements Observer {
- observeDisplayBody(null);
+ @Override
+ public void onChanged(Recipient recipient) {
+ present(conversationMessage);
+ }
}
- @Override
- public void onChanged(SpannableString update) {
- setBodyText(update);
+ private final class UpdateObserver implements Observer {
+
+ @Override
+ public void onChanged(SpannableString update) {
+ setBodyText(update);
+ }
}
private class InternalClickListener implements View.OnClickListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewHolder.java
deleted file mode 100644
index b05b899fc..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewHolder.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.thoughtcrime.securesms.conversation.ui.mentions;
-
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.components.AvatarImageView;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.MappingAdapter;
-import org.thoughtcrime.securesms.util.MappingViewHolder;
-
-public class MentionViewHolder extends MappingViewHolder {
-
- private final AvatarImageView avatar;
- private final TextView name;
-
- @Nullable private final MentionEventsListener mentionEventsListener;
-
- public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
- super(itemView);
- this.mentionEventsListener = mentionEventsListener;
-
- avatar = findViewById(R.id.mention_recipient_avatar);
- name = findViewById(R.id.mention_recipient_name);
- }
-
- @Override
- public void bind(@NonNull MentionViewState model) {
- avatar.setRecipient(model.getRecipient());
- name.setText(model.getName(context));
- itemView.setOnClickListener(v -> {
- if (mentionEventsListener != null) {
- mentionEventsListener.onMentionClicked(model.getRecipient());
- }
- });
- }
-
- public interface MentionEventsListener {
- void onMentionClicked(@NonNull Recipient recipient);
- }
-
- public static MappingAdapter.Factory createFactory(@Nullable MentionEventsListener mentionEventsListener) {
- return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewState.java
index 8a596c535..72f9a7542 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewState.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionViewState.java
@@ -1,17 +1,11 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
-import android.content.Context;
-
import androidx.annotation.NonNull;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.MappingModel;
-import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
-import java.util.Objects;
-
-public final class MentionViewState implements MappingModel {
+public final class MentionViewState extends RecipientMappingModel {
private final Recipient recipient;
@@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel {
this.recipient = recipient;
}
- @NonNull String getName(@NonNull Context context) {
- return recipient.getDisplayName(context);
- }
-
- @NonNull Recipient getRecipient() {
+ @Override
+ public @NonNull Recipient getRecipient() {
return recipient;
}
-
- @Override
- public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
- return recipient.getId().equals(newItem.recipient.getId());
- }
-
- @Override
- public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
- Context context = ApplicationDependencies.getApplication();
- return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
- Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerAdapter.java
index eb4b3be6c..06682cad3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerAdapter.java
@@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingModel;
+import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
+import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder.EventListener;
import java.util.List;
public class MentionsPickerAdapter extends MappingAdapter {
private final Runnable currentListChangedListener;
- public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) {
+ public MentionsPickerAdapter(@Nullable EventListener listener, @NonNull Runnable currentListChangedListener) {
this.currentListChangedListener = currentListChangedListener;
- registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
+ registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java
index 34d60392f..861f54053 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java
@@ -12,8 +12,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
-import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
private final MutableLiveData liveRecipient;
private final MutableLiveData liveQuery;
private final MutableLiveData isShowing;
- private final MegaphoneRepository megaphoneRepository;
- MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
- @NonNull MegaphoneRepository megaphoneRepository)
- {
- this.megaphoneRepository = megaphoneRepository;
+ MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
this.liveRecipient = new MutableLiveData<>();
this.liveQuery = new MutableLiveData<>();
this.selectedRecipient = new SingleLiveEvent<>();
@@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
void onSelectionChange(@NonNull Recipient recipient) {
selectedRecipient.setValue(recipient);
- megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
}
void setIsShowing(boolean isShowing) {
@@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
@Override
public @NonNull T create(@NonNull Class modelClass) {
//noinspection ConstantConditions
- return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()),
- ApplicationDependencies.getMegaphoneRepository()));
+ return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java
index 8033359ab..efc7f6b30 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java
@@ -123,11 +123,13 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
@SuppressLint("StaticFieldLeak")
@Override
protected void onItemSwiped(long threadId, int unreadCount) {
- new SnackbarAsyncTask(getView(),
- getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
- getString(R.string.ConversationListFragment_undo),
- getResources().getColor(R.color.amber_500),
- Snackbar.LENGTH_LONG, false)
+ new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(),
+ requireView(),
+ getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
+ getString(R.string.ConversationListFragment_undo),
+ getResources().getColor(R.color.amber_500),
+ Snackbar.LENGTH_LONG,
+ false)
{
@Override
protected void executeAction(@Nullable Long parameter) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index 515c29136..362931e44 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -55,6 +55,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.ViewCompat;
+import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -223,7 +224,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
list.setItemAnimator(new DeleteItemAnimator());
list.addOnScrollListener(new ScrollListener());
- snapToTopDataObserver = new SnapToTopDataObserver(list, null);
+ snapToTopDataObserver = new SnapToTopDataObserver(list);
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
@@ -367,7 +368,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onContactClicked(@NonNull Recipient contact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
- return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
+ return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact.getId());
}, threadId -> {
hideKeyboard();
getNavigator().goToConversation(contact.getId(),
@@ -423,6 +424,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
viewModel.onMegaphoneCompleted(event);
}
+ @Override
+ public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
+ dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
+ }
+
private void onReminderAction(@IdRes int reminderActionId) {
if (reminderActionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
@@ -679,7 +685,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
int count = selectedConversations.size();
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
- new SnackbarAsyncTask(getView(),
+ new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(),
+ requireView(),
snackBarTitle,
getString(R.string.ConversationListFragment_undo),
getResources().getColor(R.color.amber_500),
@@ -1002,11 +1009,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@SuppressLint("StaticFieldLeak")
protected void onItemSwiped(long threadId, int unreadCount) {
- new SnackbarAsyncTask(getView(),
- getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
- getString(R.string.ConversationListFragment_undo),
- getResources().getColor(R.color.amber_500),
- Snackbar.LENGTH_LONG, false)
+ new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(),
+ requireView(),
+ getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
+ getString(R.string.ConversationListFragment_undo),
+ getResources().getColor(R.color.amber_500),
+ Snackbar.LENGTH_LONG,
+ false)
{
@Override
protected void executeAction(@Nullable Long parameter) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
index 538f9a25a..491583ad3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
@@ -453,11 +453,13 @@ public final class ConversationListItem extends RelativeLayout
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(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context))));
- }
+ return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
+ if (r.isGroup()) {
+ return context.getString(R.string.ThreadRecord_safety_number_changed);
+ } else {
+ return context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(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())) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java
index ed4395274..ed4a2622f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+import com.annimon.stream.Stream;
+
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
@@ -27,6 +29,11 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
public class UnidentifiedAccessUtil {
@@ -42,34 +49,64 @@ public class UnidentifiedAccessUtil {
}
@WorkerThread
- public static Optional getAccessFor(@NonNull Context context,
- @NonNull Recipient recipient)
- {
- try {
- byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
- byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
- byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient);
+ public static Optional getAccessFor(@NonNull Context context, @NonNull Recipient recipient) {
+ return getAccessFor(context, recipient, true);
+ }
- if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
- ourUnidentifiedAccessKey = Util.getSecretBytes(16);
- }
+ @WorkerThread
+ public static Optional getAccessFor(@NonNull Context context, @NonNull Recipient recipient, boolean log) {
+ return getAccessFor(context, Collections.singletonList(recipient), log).get(0);
+ }
- Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
- " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
- " | UUID certificate supported? " + recipient.isUuidSupported());
+ @WorkerThread
+ public static List> getAccessFor(@NonNull Context context, @NonNull List recipients) {
+ return getAccessFor(context, recipients, true);
+ }
+
+ @WorkerThread
+ public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean log) {
+ byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
+
+ if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
+ ourUnidentifiedAccessKey = Util.getSecretBytes(16);
+ }
+
+ List> access = new ArrayList<>(recipients.size());
+
+ Map typeCounts = new HashMap<>();
+
+ for (Recipient recipient : recipients) {
+ byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
+ CertificateType certificateType = getUnidentifiedAccessCertificateType(recipient);
+ byte[] ourUnidentifiedAccessCertificate = SignalStore.certificateValues().getUnidentifiedAccessCertificate(certificateType);
+
+ int typeCount = Util.getOrDefault(typeCounts, certificateType, 0);
+ typeCount++;
+ typeCounts.put(certificateType, typeCount);
if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
- return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
- ourUnidentifiedAccessCertificate),
- new UnidentifiedAccess(ourUnidentifiedAccessKey,
- ourUnidentifiedAccessCertificate)));
+ try {
+ access.add(Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
+ ourUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(ourUnidentifiedAccessKey,
+ ourUnidentifiedAccessCertificate))));
+ } catch (InvalidCertificateException e) {
+ Log.w(TAG, e);
+ access.add(Optional.absent());
+ }
+ } else {
+ access.add(Optional.absent());
}
-
- return Optional.absent();
- } catch (InvalidCertificateException e) {
- Log.w(TAG, e);
- return Optional.absent();
}
+
+ int unidentifiedCount = Stream.of(access).filter(Optional::isPresent).toList().size();
+ int otherCount = access.size() - unidentifiedCount;
+
+ if (log) {
+ Log.i(TAG, "Unidentified: " + unidentifiedCount + ", Other: " + otherCount + ". Types: " + typeCounts);
+ }
+
+ return access;
}
public static Optional getAccessForSync(@NonNull Context context) {
@@ -95,21 +132,20 @@ public class UnidentifiedAccessUtil {
}
}
- private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
- CertificateType certificateType;
+ private static @NonNull CertificateType getUnidentifiedAccessCertificateType(@NonNull Recipient recipient) {
PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode();
switch (sendPhoneNumberTo) {
- case EVERYONE: certificateType = CertificateType.UUID_AND_E164; break;
- case CONTACTS: certificateType = recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY; break;
- case NOBODY : certificateType = CertificateType.UUID_ONLY; break;
+ case EVERYONE: return CertificateType.UUID_AND_E164;
+ case CONTACTS: return recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY;
+ case NOBODY : return CertificateType.UUID_ONLY;
default : throw new AssertionError();
}
+ }
- Log.i(TAG, String.format("Certificate type for %s with setting %s -> %s", recipient.getId(), sendPhoneNumberTo, certificateType));
-
+ private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
return SignalStore.certificateValues()
- .getUnidentifiedAccessCertificate(certificateType);
+ .getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType(recipient));
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index f60e91285..daf8f71c3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -19,11 +19,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.media.MediaDataSource;
-import android.media.MediaMetadataRetriever;
import android.net.Uri;
-import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
@@ -58,14 +55,10 @@ import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
-import org.thoughtcrime.securesms.util.BitmapDecodingException;
-import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
-import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
-import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -74,7 +67,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -90,9 +82,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
public class AttachmentDatabase extends Database {
@@ -111,8 +100,6 @@ public class AttachmentDatabase extends Database {
private static final String TRANSFER_FILE = "transfer_file";
public static final String SIZE = "data_size";
static final String FILE_NAME = "file_name";
- public static final String THUMBNAIL = "thumbnail";
- static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note";
@@ -124,7 +111,6 @@ public class AttachmentDatabase extends Database {
static final String STICKER_EMOJI = "sticker_emoji";
static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
public static final String DATA_RANDOM = "data_random";
- private static final String THUMBNAIL_RANDOM = "thumbnail_random";
static final String WIDTH = "width";
static final String HEIGHT = "height";
static final String CAPTION = "caption";
@@ -149,11 +135,10 @@ public class AttachmentDatabase extends Database {
private static final String[] PROJECTION = new String[] {ROW_ID,
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
- CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL,
- TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL,
- THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
+ CDN_NUMBER, CONTENT_LOCATION, DATA,
+ TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST,
FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM,
- THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
+ WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH,
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
UPLOAD_TIMESTAMP };
@@ -175,15 +160,12 @@ public class AttachmentDatabase extends Database {
DATA + " TEXT, " +
SIZE + " INTEGER, " +
FILE_NAME + " TEXT, " +
- THUMBNAIL + " TEXT, " +
- THUMBNAIL_ASPECT_RATIO + " REAL, " +
UNIQUE_ID + " INTEGER NOT NULL, " +
DIGEST + " BLOB, " +
FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " +
BORDERLESS + " INTEGER DEFAULT 0, " +
DATA_RANDOM + " BLOB, " +
- THUMBNAIL_RANDOM + " BLOB, " +
QUOTE + " INTEGER DEFAULT 0, " +
WIDTH + " INTEGER DEFAULT 0, " +
HEIGHT + " INTEGER DEFAULT 0, " +
@@ -208,10 +190,6 @@ public class AttachmentDatabase extends Database {
"CREATE INDEX IF NOT EXISTS part_data_index ON " + TABLE_NAME + " (" + DATA + ");"
};
- private static final long STANDARD_THUMB_TIME = 1000;
-
- private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
-
private final AttachmentSecret attachmentSecret;
public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
@@ -228,29 +206,6 @@ public class AttachmentDatabase extends Database {
else return dataStream;
}
- public @NonNull InputStream getThumbnailStream(@NonNull AttachmentId attachmentId)
- throws IOException
- {
- Log.d(TAG, "getThumbnailStream(" + attachmentId + ")");
- InputStream dataStream = getDataStream(attachmentId, THUMBNAIL, 0);
-
- if (dataStream != null) {
- return dataStream;
- }
-
- try {
- InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME)).get();
-
- if (generatedStream == null) throw new FileNotFoundException("No thumbnail stream available: " + attachmentId);
- else return generatedStream;
- } catch (InterruptedException ie) {
- throw new AssertionError("interrupted");
- } catch (ExecutionException ee) {
- Log.w(TAG, ee);
- throw new IOException(ee);
- }
- }
-
public boolean containsStickerPackId(@NonNull String stickerPackId) {
String selection = STICKER_PACK_ID + " = ?";
String[] args = new String[] { stickerPackId };
@@ -365,12 +320,11 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null;
try {
- cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
+ cursor = database.query(TABLE_NAME, new String[] {DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
- cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
@@ -418,12 +372,11 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null;
try {
- cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
+ cursor = database.query(TABLE_NAME, new String[] {DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
- cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
@@ -437,8 +390,6 @@ public class AttachmentDatabase extends Database {
values.put(DATA, (String) null);
values.put(DATA_RANDOM, (byte[]) null);
values.put(DATA_HASH, (String) null);
- values.put(THUMBNAIL, (String) null);
- values.put(THUMBNAIL_RANDOM, (byte[]) null);
values.put(FILE_NAME, (String) null);
values.put(CAPTION, (String) null);
values.put(SIZE, 0);
@@ -463,7 +414,7 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
try (Cursor cursor = database.query(TABLE_NAME,
- new String[]{DATA, THUMBNAIL, CONTENT_TYPE},
+ new String[]{DATA, CONTENT_TYPE},
PART_ID_WHERE,
id.toStrings(),
null,
@@ -475,11 +426,10 @@ public class AttachmentDatabase extends Database {
return;
}
String data = cursor.getString(cursor.getColumnIndex(DATA));
- String thumbnail = cursor.getString(cursor.getColumnIndex(THUMBNAIL));
String contentType = cursor.getString(cursor.getColumnIndex(CONTENT_TYPE));
database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings());
- deleteAttachmentOnDisk(data, thumbnail, contentType, id);
+ deleteAttachmentOnDisk(data, contentType, id);
notifyAttachmentListeners();
}
}
@@ -502,10 +452,9 @@ public class AttachmentDatabase extends Database {
filesOnDisk.add(file.getAbsolutePath());
}
- try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA, THUMBNAIL }, null, null, null, null, null, null)) {
+ try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA }, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
filesInDb.add(CursorUtil.requireString(cursor, DATA));
- filesInDb.add(CursorUtil.requireString(cursor, THUMBNAIL));
}
}
@@ -530,7 +479,6 @@ public class AttachmentDatabase extends Database {
}
private void deleteAttachmentOnDisk(@Nullable String data,
- @Nullable String thumbnail,
@Nullable String contentType,
@NonNull AttachmentId attachmentId)
{
@@ -561,8 +509,6 @@ public class AttachmentDatabase extends Database {
values.putNull(DATA);
values.putNull(DATA_RANDOM);
values.putNull(DATA_HASH);
- values.putNull(THUMBNAIL);
- values.putNull(THUMBNAIL_RANDOM);
deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings());
}
database.setTransactionSuccessful();
@@ -581,15 +527,7 @@ public class AttachmentDatabase extends Database {
}
}
- if (!TextUtils.isEmpty(thumbnail)) {
- if (new File(thumbnail).delete()) {
- Log.i(TAG, "[deleteAttachmentOnDisk] Deleted thumbnail. " + data + " " + attachmentId);
- } else {
- Log.w(TAG, "[deleteAttachmentOnDisk] Failed to delete attachment. " + data + " " + attachmentId);
- }
- }
-
- if (MediaUtil.isImageType(contentType) || thumbnail != null) {
+ if (MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)) {
Glide.get(context).clearDiskCache();
}
}
@@ -623,22 +561,17 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
DataInfo oldInfo = getAttachmentDataFileInfo(attachmentId, DATA);
- DataInfo dataInfo = setAttachmentData(inputStream, false, attachmentId);
+ DataInfo dataInfo = setAttachmentData(inputStream, attachmentId);
File transferFile = getTransferFile(databaseHelper.getReadableDatabase(), attachmentId);
if (oldInfo != null) {
updateAttachmentDataHash(database, oldInfo.hash, dataInfo);
}
- if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) {
- values.put(THUMBNAIL, dataInfo.file.getAbsolutePath());
- values.put(THUMBNAIL_RANDOM, dataInfo.random);
- } else {
- values.put(DATA, dataInfo.file.getAbsolutePath());
- values.put(SIZE, dataInfo.length);
- values.put(DATA_RANDOM, dataInfo.random);
- values.put(DATA_HASH, dataInfo.hash);
- }
+ values.put(DATA, dataInfo.file.getAbsolutePath());
+ values.put(SIZE, dataInfo.length);
+ values.put(DATA_RANDOM, dataInfo.random);
+ values.put(DATA_HASH, dataInfo.hash);
String visualHashString = getVisualHashStringOrNull(placeholder);
if (visualHashString != null) {
@@ -662,8 +595,6 @@ public class AttachmentDatabase extends Database {
//noinspection ResultOfMethodCallIgnored
transferFile.delete();
}
-
- thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
}
private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
@@ -846,7 +777,6 @@ public class AttachmentDatabase extends Database {
DataInfo dataInfo = setAttachmentData(destination,
mediaStream.getStream(),
- false,
databaseAttachment.getAttachmentId());
ContentValues contentValues = new ContentValues();
@@ -1055,16 +985,8 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
- String randomColumn;
-
- switch (dataType) {
- case DATA: randomColumn = DATA_RANDOM; break;
- case THUMBNAIL: randomColumn = THUMBNAIL_RANDOM; break;
- default:throw new AssertionError("Unknown data type: " + dataType);
- }
-
try {
- cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, randomColumn, DATA_HASH}, PART_ID_WHERE, attachmentId.toStrings(),
+ cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, DATA_RANDOM, DATA_HASH}, PART_ID_WHERE, attachmentId.toStrings(),
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
@@ -1074,7 +996,7 @@ public class AttachmentDatabase extends Database {
return new DataInfo(new File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
- cursor.getBlob(cursor.getColumnIndexOrThrow(randomColumn)),
+ cursor.getBlob(cursor.getColumnIndexOrThrow(DATA_RANDOM)),
cursor.getString(cursor.getColumnIndexOrThrow(DATA_HASH)));
} else {
return null;
@@ -1087,26 +1009,24 @@ public class AttachmentDatabase extends Database {
}
private @NonNull DataInfo setAttachmentData(@NonNull Uri uri,
- boolean isThumbnail,
@Nullable AttachmentId attachmentId)
throws MmsException
{
try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
- return setAttachmentData(inputStream, isThumbnail, attachmentId);
+ return setAttachmentData(inputStream, attachmentId);
} catch (IOException e) {
throw new MmsException(e);
}
}
private @NonNull DataInfo setAttachmentData(@NonNull InputStream in,
- boolean isThumbnail,
@Nullable AttachmentId attachmentId)
throws MmsException
{
try {
File dataFile = newFile();
- return setAttachmentData(dataFile, in, isThumbnail, attachmentId);
+ return setAttachmentData(dataFile, in, attachmentId);
} catch (IOException e) {
throw new MmsException(e);
}
@@ -1119,7 +1039,6 @@ public class AttachmentDatabase extends Database {
private @NonNull DataInfo setAttachmentData(@NonNull File destination,
@NonNull InputStream in,
- boolean isThumbnail,
@Nullable AttachmentId attachmentId)
throws MmsException
{
@@ -1130,18 +1049,16 @@ public class AttachmentDatabase extends Database {
long length = Util.copy(digestInputStream, out.second);
String hash = Base64.encodeBytes(digestInputStream.getMessageDigest().digest());
- if (!isThumbnail) {
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- Optional sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId);
- if (sharedDataInfo.isPresent()) {
- Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath());
- if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) {
- Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
- }
- return sharedDataInfo.get();
- } else {
- Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ Optional sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId);
+ if (sharedDataInfo.isPresent()) {
+ Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath());
+ if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) {
+ Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
}
+ return sharedDataInfo.get();
+ } else {
+ Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
}
return new DataInfo(destination, length, out.first, hash);
@@ -1216,7 +1133,7 @@ public class AttachmentDatabase extends Database {
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)),
+ MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType,
object.getInt(TRANSFER_STATE),
object.getLong(SIZE),
@@ -1254,7 +1171,7 @@ public class AttachmentDatabase extends Database {
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
- !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
+ MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType,
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
@@ -1296,10 +1213,9 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo dataInfo = null;
long uniqueId = System.currentTimeMillis();
- long thumbnailTimeUs;
- if (attachment.getDataUri() != null) {
- dataInfo = setAttachmentData(attachment.getDataUri(), false, null);
+ if (attachment.getUri() != null) {
+ dataInfo = setAttachmentData(attachment.getUri(), null);
Log.d(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath());
}
@@ -1342,11 +1258,9 @@ public class AttachmentDatabase extends Database {
if (attachment.getTransformProperties().isVideoEdited()) {
contentValues.putNull(VISUAL_HASH);
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
- thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
} else {
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
- thumbnailTimeUs = STANDARD_THUMB_TIME;
}
if (attachment.isSticker()) {
@@ -1370,38 +1284,6 @@ public class AttachmentDatabase extends Database {
boolean notifyPacks = attachment.isSticker() && !hasStickerAttachments();
long rowId = database.insert(TABLE_NAME, null, contentValues);
AttachmentId attachmentId = new AttachmentId(rowId, uniqueId);
- Uri thumbnailUri = attachment.getThumbnailUri();
- boolean hasThumbnail = false;
-
- if (thumbnailUri != null) {
- try (InputStream attachmentStream = PartAuthority.getAttachmentStream(context, thumbnailUri)) {
- Pair dimens = BitmapUtil.getDimensions(attachmentStream);
- updateAttachmentThumbnail(attachmentId,
- PartAuthority.getAttachmentStream(context, thumbnailUri),
- (float) dimens.first / (float) dimens.second);
- hasThumbnail = true;
- } catch (IOException | BitmapDecodingException e) {
- Log.w(TAG, "Failed to save existing thumbnail.", e);
- }
- }
-
- if (!hasThumbnail && dataInfo != null) {
- if (MediaUtil.hasVideoThumbnail(attachment.getDataUri()) && thumbnailTimeUs == STANDARD_THUMB_TIME) {
- Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri(), thumbnailTimeUs);
-
- if (bitmap != null) {
- try (ThumbnailData thumbnailData = new ThumbnailData(bitmap)) {
- updateAttachmentThumbnail(attachmentId, thumbnailData.toDataStream(), thumbnailData.getAspectRatio());
- }
- } else {
- Log.w(TAG, "Retrieving video thumbnail failed, submitting thumbnail generation job...");
- thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
- }
- } else {
- Log.i(TAG, "Submitting thumbnail generation job...");
- thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
- }
- }
if (notifyPacks) {
notifyStickerPackListeners();
@@ -1423,35 +1305,6 @@ public class AttachmentDatabase extends Database {
return null;
}
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- protected void updateAttachmentThumbnail(AttachmentId attachmentId, InputStream in, float aspectRatio)
- throws MmsException
- {
- Log.i(TAG, "updating part thumbnail for #" + attachmentId);
-
- DataInfo thumbnailFile = setAttachmentData(in, true, attachmentId);
-
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- ContentValues values = new ContentValues(2);
-
- values.put(THUMBNAIL, thumbnailFile.file.getAbsolutePath());
- values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio);
- values.put(THUMBNAIL_RANDOM, thumbnailFile.random);
-
- database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
-
- Cursor cursor = database.query(TABLE_NAME, new String[] {MMS_ID}, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
-
- try {
- if (cursor != null && cursor.moveToFirst()) {
- notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID))));
- }
- } finally {
- if (cursor != null) cursor.close();
- }
- }
-
@WorkerThread
public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) {
Log.i(TAG, "updating part audio wave form for #" + attachmentId);
@@ -1468,66 +1321,6 @@ public class AttachmentDatabase extends Database {
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
}
- @VisibleForTesting
- class ThumbnailFetchCallable implements Callable {
-
- private final AttachmentId attachmentId;
- private final long timeUs;
-
- ThumbnailFetchCallable(AttachmentId attachmentId, long timeUs) {
- this.attachmentId = attachmentId;
- this.timeUs = timeUs;
- }
-
- @Override
- public @Nullable InputStream call() throws Exception {
- Log.d(TAG, "Executing thumbnail job...");
- final InputStream stream = getDataStream(attachmentId, THUMBNAIL, 0);
-
- if (stream != null) {
- return stream;
- }
-
- DatabaseAttachment attachment = getAttachment(attachmentId);
-
- if (attachment == null || !attachment.hasData()) {
- return null;
- }
-
- if (MediaUtil.isVideoType(attachment.getContentType())) {
-
- try (ThumbnailData data = generateVideoThumbnail(attachmentId, timeUs)) {
-
- if (data != null) {
- updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio());
-
- return getDataStream(attachmentId, THUMBNAIL, 0);
- }
- }
- }
-
- return null;
- }
-
- private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId, long timeUs) throws IOException {
- if (Build.VERSION.SDK_INT < 23) {
- Log.w(TAG, "Video thumbnails not supported...");
- return null;
- }
-
- try (MediaDataSource dataSource = mediaDataSourceFor(attachmentId)) {
- if (dataSource == null) return null;
-
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource);
-
- Bitmap bitmap = retriever.getFrameAtTime(timeUs);
-
- Log.i(TAG, "Generated video thumbnail...");
- return bitmap != null ? new ThumbnailData(bitmap) : null;
- }
- }
- }
@RequiresApi(23)
public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseContentProviders.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseContentProviders.java
index da62f5fd2..ed22c13cc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseContentProviders.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseContentProviders.java
@@ -4,9 +4,12 @@ import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.thoughtcrime.securesms.BuildConfig;
+
/**
* Starting in API 26, a {@link ContentProvider} needs to be defined for each authority you wish to
* observe changes on. These classes essentially do nothing except exist so Android doesn't complain.
@@ -14,11 +17,15 @@ import androidx.annotation.Nullable;
public class DatabaseContentProviders {
public static class ConversationList extends NoopContentProvider {
- public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.conversationlist");
+ private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversationlist";
+ private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
+
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
public static class Conversation extends NoopContentProvider {
- private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.securesms.database.conversation/";
+ private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversation";
+ private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/";
public static Uri getUriForThread(long threadId) {
return Uri.parse(CONTENT_URI_STRING + threadId);
@@ -34,15 +41,24 @@ public class DatabaseContentProviders {
}
public static class Attachment extends NoopContentProvider {
- public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.attachment");
+ private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.attachment";
+ private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
+
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
public static class Sticker extends NoopContentProvider {
- public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.sticker");
+ private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.sticker";
+ private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
+
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
public static class StickerPack extends NoopContentProvider {
- public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.stickerpack");
+ private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.stickerpack";
+ private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
+
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
private static abstract class NoopContentProvider extends ContentProvider {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java b/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java
index edca8df42..84f826c0e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java
@@ -22,8 +22,6 @@ public class EarlyReceiptCache {
}
public synchronized void increment(long timestamp, @NonNull RecipientId origin) {
- Log.i(TAG, String.format(Locale.US, "[%s] Timestamp: %d, Recipient: %s", name, timestamp, origin.serialize()));
-
Map receipts = cache.get(timestamp);
if (receipts == null) {
@@ -43,10 +41,6 @@ public class EarlyReceiptCache {
public synchronized Map remove(long timestamp) {
Map receipts = cache.remove(timestamp);
-
- Log.i(TAG, this+"");
- Log.i(TAG, String.format(Locale.US, "Checking early receipts (%d): %d", timestamp, receipts == null ? 0 : receipts.size()));
-
return receipts != null ? receipts : new HashMap<>();
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
index 3bc355628..57e4d114c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -23,14 +23,12 @@ public class MediaDatabase extends Database {
private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
- + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
- + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", "
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
index d6dfceb63..6e7ae2e62 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
@@ -126,7 +126,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract @NonNull Pair insertReceivedCall(@NonNull RecipientId address);
public abstract @NonNull Pair insertOutgoingCall(@NonNull RecipientId address);
- public abstract @NonNull Pair insertMissedCall(@NonNull RecipientId address);
+ public abstract @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp);
public abstract Optional insertMessageInbox(IncomingTextMessage message, long type);
public abstract Optional insertMessageInbox(IncomingTextMessage message);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
index 6274398d2..7bd9babeb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -213,7 +213,6 @@ public class MmsDatabase extends MessageDatabase {
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
- "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
@@ -390,7 +389,7 @@ public class MmsDatabase extends MessageDatabase {
}
@Override
- public @NonNull Pair insertMissedCall(@NonNull RecipientId address) {
+ public @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp) {
throw new UnsupportedOperationException();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 03ef88f96..2ad3372fc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -411,7 +411,6 @@ public class MmsSmsDatabase extends Database {
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
- "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 68d34d02f..ff123c04e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -10,7 +10,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
-import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase;
@@ -152,14 +151,14 @@ public class RecipientDatabase extends Database {
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
- private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
+ private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
new String[] { TABLE_NAME + "." + ID,
TABLE_NAME + "." + STORAGE_PROTO },
TYPED_RECIPIENT_PROJECTION,
new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
- });
+ }).flatMap(Stream::of).toArray(String[]::new);
public static final String[] CREATE_INDEXS = new String[] {
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
@@ -395,10 +394,6 @@ public class RecipientDatabase extends Database {
throw new IllegalArgumentException("Must provide a UUID or E164!");
}
- if (!FeatureFlags.cds()) {
- highTrust = true;
- }
-
RecipientId recipientNeedingRefresh = null;
Pair remapped = null;
boolean transactionSuccessful = false;
@@ -1000,7 +995,7 @@ public class RecipientDatabase extends Database {
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
- if (contact.isProfileSharingEnabled() && isInsert) {
+ if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) {
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
}
@@ -1055,8 +1050,8 @@ public class RecipientDatabase extends Database {
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
List out = new ArrayList<>();
- String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION,
- new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY });
+ String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
+ new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }).flatMap(Stream::of).toArray(String[]::new);
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
@@ -1080,7 +1075,7 @@ public class RecipientDatabase extends Database {
public @NonNull Map getContactStorageSyncIdsMap() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ? AND " + GROUP_TYPE + " != ?";
- String[] args = { String.valueOf(DirtyState.DELETE), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) };
+ String[] args = { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) };
Map out = new HashMap<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) {
@@ -1802,6 +1797,12 @@ public class RecipientDatabase extends Database {
}
}
+ /**
+ * Handles inserts the (e164, UUID) pairs, which could result in merges. Does not mark users as
+ * registered.
+ *
+ * @return A mapping of (RecipientId, UUID)
+ */
public @NonNull Map bulkProcessCdsResult(@NonNull Map mapping) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
HashMap uuidMap = new HashMap<>();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java
index a25096f33..751e1055c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java
@@ -12,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -145,6 +146,16 @@ public class SessionDatabase extends Database {
database.delete(TABLE_NAME, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()});
}
+ public boolean hasSessionFor(@NonNull RecipientId recipientId) {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ String query = RECIPIENT_ID + " = ?";
+ String[] args = SqlUtil.buildArgs(recipientId);
+
+ try (Cursor cursor = database.query(TABLE_NAME, new String[] { ID }, query, args, null, null, null, "1")) {
+ return cursor != null && cursor.moveToFirst();
+ }
+ }
+
public static final class SessionRow {
private final RecipientId recipientId;
private final int deviceId;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index 5fcc14e44..5e25a2687 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -644,20 +644,20 @@ public class SmsDatabase extends MessageDatabase {
@Override
public @NonNull Pair insertReceivedCall(@NonNull RecipientId address) {
- return insertCallLog(address, Types.INCOMING_CALL_TYPE, false);
+ return insertCallLog(address, Types.INCOMING_CALL_TYPE, false, System.currentTimeMillis());
}
@Override
public @NonNull Pair insertOutgoingCall(@NonNull RecipientId address) {
- return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false);
+ return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false, System.currentTimeMillis());
}
@Override
- public @NonNull Pair insertMissedCall(@NonNull RecipientId address) {
- return insertCallLog(address, Types.MISSED_CALL_TYPE, true);
+ public @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp) {
+ return insertCallLog(address, Types.MISSED_CALL_TYPE, true, timestamp);
}
- private @NonNull Pair insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread) {
+ private @NonNull Pair insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
Recipient recipient = Recipient.resolved(recipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
@@ -665,7 +665,7 @@ public class SmsDatabase extends MessageDatabase {
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, System.currentTimeMillis());
- values.put(DATE_SENT, System.currentTimeMillis());
+ values.put(DATE_SENT, timestamp);
values.put(READ, unread ? 0 : 1);
values.put(TYPE, type);
values.put(THREAD_ID, threadId);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java
index 37a0983cf..3aed20a7f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java
@@ -69,10 +69,8 @@ public final class ThreadBodyUtil {
} 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 getBody(context, record);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 4ace9efa3..7c4651ef9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -872,22 +872,17 @@ public class ThreadDatabase extends Database {
deleteAllThreads();
}
- public long getThreadIdIfExistsFor(Recipient recipient) {
+ public long getThreadIdIfExistsFor(@NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = RECIPIENT_ID + " = ?";
- String[] recipientsArg = new String[] {recipient.getId().serialize()};
- Cursor cursor = null;
+ String[] recipientsArg = new String[] {recipientId.serialize()};
- try {
- cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
-
- if (cursor != null && cursor.moveToFirst())
- return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
- else
- return -1L;
- } finally {
- if (cursor != null)
- cursor.close();
+ try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID }, where, recipientsArg, null, null, null, "1")) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return CursorUtil.requireLong(cursor, ID);
+ } else {
+ return -1;
+ }
}
}
@@ -950,6 +945,10 @@ public class ThreadDatabase extends Database {
return Recipient.resolved(id);
}
+ public boolean hasThread(@NonNull RecipientId recipientId) {
+ return getThreadIdIfExistsFor(recipientId) > -1;
+ }
+
public void setHasSent(long threadId, boolean hasSent) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(HAS_SENT, hasSent ? 1 : 0);
@@ -1095,7 +1094,7 @@ public class ThreadDatabase extends Database {
Slide thumbnail = Optional.fromNullable(slideDeck.getThumbnailSlide()).or(Optional.fromNullable(slideDeck.getStickerSlide())).orNull();
if (thumbnail != null && !((MmsMessageRecord) record).isViewOnce()) {
- return thumbnail.getThumbnailUri();
+ return thumbnail.getUri();
}
return null;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 284c8bbd2..1551e3b94 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.Base64;
+import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -146,8 +147,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int UNKNOWN_STORAGE_FIELDS = 71;
private static final int STICKER_CONTENT_TYPE = 72;
private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73;
+ private static final int THUMBNAIL_CLEANUP = 74;
+ private static final int STICKER_CONTENT_TYPE_CLEANUP = 75;
- private static final int DATABASE_VERSION = 73;
+ private static final int DATABASE_VERSION = 75;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -1023,6 +1026,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN sticker_emoji TEXT DEFAULT NULL");
}
+ if (oldVersion < THUMBNAIL_CLEANUP) {
+ int total = 0;
+ int deleted = 0;
+
+ try (Cursor cursor = db.rawQuery("SELECT thumbnail FROM part WHERE thumbnail NOT NULL", null)) {
+ if (cursor != null) {
+ total = cursor.getCount();
+ Log.w(TAG, "Found " + total + " thumbnails to delete.");
+ }
+
+ while (cursor != null && cursor.moveToNext()) {
+ File file = new File(CursorUtil.requireString(cursor, "thumbnail"));
+
+ if (file.delete()) {
+ deleted++;
+ } else {
+ Log.w(TAG, "Failed to delete file! " + file.getAbsolutePath());
+ }
+ }
+ }
+
+ Log.w(TAG, "Deleted " + deleted + "/" + total + " thumbnail files.");
+ }
+
+ if (oldVersion < STICKER_CONTENT_TYPE_CLEANUP) {
+ ContentValues values = new ContentValues();
+ values.put("ct", "image/webp");
+
+ String query = "sticker_id NOT NULL AND (ct IS NULL OR ct = '')";
+
+ int rows = db.update("part", values, query, null);
+ Log.i(TAG, "Updated " + rows + " sticker attachment content types.");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 19906c3fe..503d217fa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -270,7 +270,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
public long getTimestamp() {
- if (isPush() && getDateSent() < getDateReceived()) {
+ if ((isPush() || isCallLog()) && getDateSent() < getDateReceived()) {
return getDateSent();
}
return getDateReceived();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java
index 2586ea620..8f6773c8f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.Util;
import java.util.Objects;
@@ -68,7 +69,7 @@ public final class StickerRecord {
}
public @NonNull String getContentType() {
- return contentType == null ? MediaUtil.IMAGE_WEBP : contentType;
+ return Util.isEmpty(contentType) ? MediaUtil.IMAGE_WEBP : contentType;
}
public long getSize() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
index 1f76dbd21..a4adbec66 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
@@ -138,8 +138,7 @@ public class ApplicationDependencies {
messageSender.update(
IncomingMessageObserver.getPipe(),
IncomingMessageObserver.getUnidentifiedPipe(),
- TextSecurePreferences.isMultiDevice(application),
- FeatureFlags.attachmentsV3());
+ TextSecurePreferences.isMultiDevice(application));
}
return messageSender;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
index 74349fe1b..63d18c78b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
@@ -95,7 +95,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
new SignalProtocolStoreImpl(context),
BuildConfig.SIGNAL_AGENT,
TextSecurePreferences.isMultiDevice(context),
- FeatureFlags.attachmentsV3(),
Optional.fromNullable(IncomingMessageObserver.getPipe()),
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java
new file mode 100644
index 000000000..863bf82c0
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java
@@ -0,0 +1,119 @@
+package org.thoughtcrime.securesms.events;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.ringrtc.CameraState;
+import org.whispersystems.libsignal.IdentityKey;
+
+import java.util.Objects;
+
+public class CallParticipant {
+
+ public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false);
+
+ private final @NonNull CameraState cameraState;
+ private final @NonNull Recipient recipient;
+ private final @Nullable IdentityKey identityKey;
+ private final @NonNull BroadcastVideoSink videoSink;
+ private final boolean videoEnabled;
+ private final boolean microphoneEnabled;
+
+ public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
+ @NonNull BroadcastVideoSink renderer,
+ boolean microphoneEnabled)
+ {
+ return new CallParticipant(Recipient.self(),
+ null,
+ renderer,
+ cameraState,
+ cameraState.isEnabled() && cameraState.getCameraCount() > 0,
+ microphoneEnabled);
+ }
+
+ public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
+ @Nullable IdentityKey identityKey,
+ @NonNull BroadcastVideoSink renderer,
+ boolean videoEnabled)
+ {
+ return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
+ }
+
+ private CallParticipant(@NonNull Recipient recipient,
+ @Nullable IdentityKey identityKey,
+ @NonNull BroadcastVideoSink videoSink,
+ @NonNull CameraState cameraState,
+ boolean videoEnabled,
+ boolean microphoneEnabled)
+ {
+ this.recipient = recipient;
+ this.identityKey = identityKey;
+ this.videoSink = videoSink;
+ this.cameraState = cameraState;
+ this.videoEnabled = videoEnabled;
+ this.microphoneEnabled = microphoneEnabled;
+ }
+
+ public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
+ return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
+ }
+
+ public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
+ return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
+ }
+
+ public @NonNull Recipient getRecipient() {
+ return recipient;
+ }
+
+ public @Nullable IdentityKey getIdentityKey() {
+ return identityKey;
+ }
+
+ public @NonNull BroadcastVideoSink getVideoSink() {
+ return videoSink;
+ }
+
+ public @NonNull CameraState getCameraState() {
+ return cameraState;
+ }
+
+ public boolean isVideoEnabled() {
+ return videoEnabled;
+ }
+
+ public boolean isMicrophoneEnabled() {
+ return microphoneEnabled;
+ }
+
+ public @NonNull CameraState.Direction getCameraDirection() {
+ if (cameraState.getActiveDirection() == CameraState.Direction.BACK) {
+ return cameraState.getActiveDirection();
+ }
+ return CameraState.Direction.FRONT;
+ }
+
+ public boolean isMoreThanOneCameraAvailable() {
+ return cameraState.getCameraCount() > 1;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CallParticipant that = (CallParticipant) o;
+ return videoEnabled == that.videoEnabled &&
+ microphoneEnabled == that.microphoneEnabled &&
+ cameraState.equals(that.cameraState) &&
+ recipient.equals(that.recipient) &&
+ Objects.equals(identityKey, that.identityKey) &&
+ Objects.equals(videoSink, that.videoSink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java
index 0a154a25e..608ee5af0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java
@@ -1,18 +1,20 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer;
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
-import org.webrtc.SurfaceViewRenderer;
-import org.whispersystems.libsignal.IdentityKey;
+
+import java.util.List;
public class WebRtcViewModel {
public enum State {
// Normal states
+ CALL_PRE_JOIN,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,
@@ -33,70 +35,34 @@ public class WebRtcViewModel {
CALL_ONGOING_ELSEWHERE
}
-
- private final @NonNull State state;
- private final @NonNull Recipient recipient;
- private final @Nullable IdentityKey identityKey;
-
- private final boolean remoteVideoEnabled;
+ private final @NonNull State state;
+ private final @NonNull Recipient recipient;
private final boolean isBluetoothAvailable;
- private final boolean isMicrophoneEnabled;
private final boolean isRemoteVideoOffer;
+ private final long callConnectedTime;
- private final CameraState localCameraState;
- private final TextureViewRenderer localRenderer;
- private final TextureViewRenderer remoteRenderer;
+ private final CallParticipant localParticipant;
+ private final List remoteParticipants;
- private final long callConnectedTime;
-
- public WebRtcViewModel(@NonNull State state,
- @NonNull Recipient recipient,
- @NonNull CameraState localCameraState,
- @NonNull TextureViewRenderer localRenderer,
- @NonNull TextureViewRenderer remoteRenderer,
- boolean remoteVideoEnabled,
- boolean isBluetoothAvailable,
- boolean isMicrophoneEnabled,
- boolean isRemoteVideoOffer,
- long callConnectedTime)
- {
- this(state,
- recipient,
- null,
- localCameraState,
- localRenderer,
- remoteRenderer,
- remoteVideoEnabled,
- isBluetoothAvailable,
- isMicrophoneEnabled,
- isRemoteVideoOffer,
- callConnectedTime);
- }
-
- public WebRtcViewModel(@NonNull State state,
- @NonNull Recipient recipient,
- @Nullable IdentityKey identityKey,
- @NonNull CameraState localCameraState,
- @NonNull TextureViewRenderer localRenderer,
- @NonNull TextureViewRenderer remoteRenderer,
- boolean remoteVideoEnabled,
- boolean isBluetoothAvailable,
- boolean isMicrophoneEnabled,
- boolean isRemoteVideoOffer,
- long callConnectedTime)
+ public WebRtcViewModel(@NonNull State state,
+ @NonNull Recipient recipient,
+ @NonNull CameraState localCameraState,
+ @NonNull BroadcastVideoSink localSink,
+ boolean isBluetoothAvailable,
+ boolean isMicrophoneEnabled,
+ boolean isRemoteVideoOffer,
+ long callConnectedTime,
+ @NonNull List remoteParticipants)
{
this.state = state;
this.recipient = recipient;
- this.localCameraState = localCameraState;
- this.localRenderer = localRenderer;
- this.remoteRenderer = remoteRenderer;
- this.identityKey = identityKey;
- this.remoteVideoEnabled = remoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
- this.isMicrophoneEnabled = isMicrophoneEnabled;
this.isRemoteVideoOffer = isRemoteVideoOffer;
this.callConnectedTime = callConnectedTime;
+ this.remoteParticipants = remoteParticipants;
+
+ localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
}
public @NonNull State getState() {
@@ -107,50 +73,28 @@ public class WebRtcViewModel {
return recipient;
}
- public @NonNull CameraState getLocalCameraState() {
- return localCameraState;
- }
-
- public @Nullable IdentityKey getIdentityKey() {
- return identityKey;
- }
-
public boolean isRemoteVideoEnabled() {
- return remoteVideoEnabled;
+ return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
}
public boolean isBluetoothAvailable() {
return isBluetoothAvailable;
}
- public boolean isMicrophoneEnabled() {
- return isMicrophoneEnabled;
- }
-
public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer;
}
- public TextureViewRenderer getLocalRenderer() {
- return localRenderer;
- }
-
- public TextureViewRenderer getRemoteRenderer() {
- return remoteRenderer;
- }
-
public long getCallConnectedTime() {
return callConnectedTime;
}
- public @NonNull String toString() {
- return "[State: " + state +
- ", recipient: " + recipient.getId().serialize() +
- ", identity: " + identityKey +
- ", remoteVideo: " + remoteVideoEnabled +
- ", localVideo: " + localCameraState.isEnabled() +
- ", isRemoteVideoOffer: " + isRemoteVideoOffer +
- ", callConnectedTime: " + callConnectedTime +
- "]";
+ public @NonNull CallParticipant getLocalParticipant() {
+ return localParticipant;
}
+
+ public @NonNull List getRemoteParticipants() {
+ return remoteParticipants;
+ }
+
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java
index b751507ee..66d6538e1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java
@@ -58,7 +58,7 @@ class EncryptedCoder {
}
}
- InputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException {
+ CipherInputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
index f790c776b..9e28f07c9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -152,9 +153,12 @@ public class CreateGroupActivity extends ContactSelectionActivity {
stopwatch.split("registered");
+ List recipientsAndSelf = new ArrayList<>(resolved);
+ recipientsAndSelf.add(Recipient.self().resolve());
+
if (FeatureFlags.groupsV2create()) {
try {
- GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(resolved);
+ GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(recipientsAndSelf);
} catch (IOException e) {
Log.w(TAG, "Failed to refresh all recipient capabilities.", e);
}
@@ -164,8 +168,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
resolved = Recipient.resolvedList(ids);
- if (Stream.of(resolved).anyMatch(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) &&
- Stream.of(resolved).anyMatch(r -> !r.hasE164()))
+ boolean gv2 = Stream.of(recipientsAndSelf).allMatch(r -> r.getGroupsV2Capability() == Recipient.Capability.SUPPORTED);
+ if (!gv2 && Stream.of(resolved).anyMatch(r -> !r.hasE164()))
{
Log.w(TAG, "Invalid GV1 group...");
ids = Collections.emptyList();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java
index 0b0589399..fa6252616 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java
@@ -24,7 +24,9 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
+import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
@@ -113,6 +115,13 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
});
groupJoinButton.setVisibility(View.VISIBLE);
break;
+ case UPDATE_LINKED_DEVICE_TO_JOIN:
+ groupJoinExplain.setText(R.string.GroupJoinUpdateRequiredBottomSheetDialogFragment_update_linked_device_message);
+ groupCancelButton.setText(android.R.string.ok);
+ groupJoinButton.setVisibility(View.GONE);
+ ApplicationDependencies.getJobManager()
+ .add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
+ break;
case LOCAL_CAN_JOIN:
groupJoinExplain.setText(details.joinRequiresAdminApproval() ? R.string.GroupJoinBottomSheetDialogFragment_admin_approval_needed
: R.string.GroupJoinBottomSheetDialogFragment_direct_join);
@@ -151,19 +160,21 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
);
}
- private static FeatureFlags.GroupJoinStatus getGroupJoinStatus() {
+ private static ExtendedGroupJoinStatus getGroupJoinStatus() {
FeatureFlags.GroupJoinStatus groupJoinStatus = FeatureFlags.clientLocalGroupJoinStatus();
- if (groupJoinStatus == FeatureFlags.GroupJoinStatus.LOCAL_CAN_JOIN) {
- if (!FeatureFlags.groupsV2() || Recipient.self().getGroupsV2Capability() == Recipient.Capability.NOT_SUPPORTED) {
- // TODO [Alan] GV2 additional copy could be presented in these cases
- return FeatureFlags.GroupJoinStatus.UPDATE_TO_JOIN;
- }
-
- return groupJoinStatus;
- }
+ switch (groupJoinStatus) {
+ case COMING_SOON : return ExtendedGroupJoinStatus.COMING_SOON;
+ case UPDATE_TO_JOIN: return ExtendedGroupJoinStatus.UPDATE_TO_JOIN;
+ case LOCAL_CAN_JOIN: {
+ if (Recipient.self().getGroupsV2Capability() != Recipient.Capability.SUPPORTED) {
+ return ExtendedGroupJoinStatus.UPDATE_LINKED_DEVICE_TO_JOIN;
+ }
- return groupJoinStatus;
+ return ExtendedGroupJoinStatus.LOCAL_CAN_JOIN;
+ }
+ default: throw new AssertionError();
+ }
}
private @NonNull String errorToMessage(@NonNull FetchGroupDetailsError error) {
@@ -201,4 +212,18 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
return new ResourceContactPhoto(R.drawable.ic_group_outline_48);
}
}
+
+ public enum ExtendedGroupJoinStatus {
+ /** No version of the client that can join V2 groups by link is in production. */
+ COMING_SOON,
+
+ /** A newer version of the client is in production that will allow joining via GV2 group links. */
+ UPDATE_TO_JOIN,
+
+ /** Locally we're using a version that can use group links, but one or more linked devices needs updating for GV2. */
+ UPDATE_LINKED_DEVICE_TO_JOIN,
+
+ /** This version of the client allows joining via GV2 group links. */
+ LOCAL_CAN_JOIN
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
index df174b5c6..f4a7ce9f8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
@@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.AvatarPreviewActivity;
+import org.thoughtcrime.securesms.InviteActivity;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity;
@@ -78,7 +79,7 @@ public class ManageGroupFragment extends LoggingFragment {
private TextView pendingAndRequestingCount;
private Toolbar toolbar;
private TextView groupName;
- private LearnMoreTextView groupV1Indicator;
+ private LearnMoreTextView groupInfoText;
private TextView memberCountUnderAvatar;
private TextView memberCountAboveList;
private AvatarImageView avatar;
@@ -139,7 +140,7 @@ public class ManageGroupFragment extends LoggingFragment {
avatar = view.findViewById(R.id.group_avatar);
toolbar = view.findViewById(R.id.toolbar);
groupName = view.findViewById(R.id.name);
- groupV1Indicator = view.findViewById(R.id.manage_group_group_v1_indicator);
+ groupInfoText = view.findViewById(R.id.manage_group_info_text);
memberCountUnderAvatar = view.findViewById(R.id.member_count);
memberCountAboveList = view.findViewById(R.id.member_count_2);
groupMemberList = view.findViewById(R.id.group_members);
@@ -176,9 +177,6 @@ public class ManageGroupFragment extends LoggingFragment {
groupLinkRow = view.findViewById(R.id.group_link_row);
groupLinkButton = view.findViewById(R.id.group_link_button);
- groupV1Indicator.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
- groupV1Indicator.setLearnMoreVisible(true);
-
return view;
}
@@ -249,7 +247,6 @@ public class ManageGroupFragment extends LoggingFragment {
viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::setText);
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
- viewModel.getShowLegacyIndicator().observe(getViewLifecycleOwner(), showLegacyIndicators -> groupV1Indicator.setVisibility(showLegacyIndicators ? View.VISIBLE : View.GONE));
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
avatar.setRecipient(groupRecipient);
@@ -376,6 +373,26 @@ public class ManageGroupFragment extends LoggingFragment {
blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE);
unblockGroup.setVisibility(canBlock ? View.GONE : View.VISIBLE);
});
+
+ viewModel.getGroupInfoMessage().observe(getViewLifecycleOwner(), message -> {
+ switch (message) {
+ case LEGACY_GROUP_LEARN_MORE:
+ groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more);
+ groupInfoText.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
+ groupInfoText.setLearnMoreVisible(true);
+ groupInfoText.setVisibility(View.VISIBLE);
+ break;
+ case MMS_WARNING:
+ groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group);
+ groupInfoText.setOnLinkClickListener(v -> startActivity(new Intent(requireContext(), InviteActivity.class)));
+ groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now);
+ groupInfoText.setVisibility(View.VISIBLE);
+ break;
+ default:
+ groupInfoText.setVisibility(View.GONE);
+ break;
+ }
+ });
}
private static int booleanToOnOff(boolean isOn) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
index c0de0992f..021e48b38 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
@@ -80,6 +80,7 @@ public class ManageGroupViewModel extends ViewModel {
private final LiveData showLegacyIndicator;
private final LiveData mentionSetting;
private final LiveData groupLinkOn;
+ private final LiveData groupInfoMessage;
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
this.context = context;
@@ -123,6 +124,16 @@ public class ManageGroupViewModel extends ViewModel {
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
+ this.groupInfoMessage = Transformations.map(this.showLegacyIndicator,
+ showLegacyInfo -> {
+ if (showLegacyInfo) {
+ return GroupInfoMessage.LEGACY_GROUP_LEARN_MORE;
+ } else if (groupId.isMms()) {
+ return GroupInfoMessage.MMS_WARNING;
+ } else {
+ return GroupInfoMessage.NONE;
+ }
+ });
}
@WorkerThread
@@ -152,10 +163,6 @@ public class ManageGroupViewModel extends ViewModel {
return fullMemberCountSummary;
}
- LiveData getShowLegacyIndicator() {
- return showLegacyIndicator;
- }
-
LiveData getGroupRecipient() {
return groupRecipient;
}
@@ -228,6 +235,10 @@ public class ManageGroupViewModel extends ViewModel {
return groupLinkOn;
}
+ LiveData getGroupInfoMessage() {
+ return groupInfoMessage;
+ }
+
void handleExpirationSelection() {
manageGroupRepository.getRecipient(groupRecipient ->
ExpirationDialog.show(context,
@@ -397,6 +408,12 @@ public class ManageGroupViewModel extends ViewModel {
}
}
+ enum GroupInfoMessage {
+ NONE,
+ LEGACY_GROUP_LEARN_MORE,
+ MMS_WARNING
+ }
+
private enum CollapseState {
OPEN,
COLLAPSED
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java
index 355a486ed..ad1a8eac9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java
@@ -11,7 +11,6 @@ import android.content.SharedPreferences;
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;
@@ -36,7 +35,15 @@ public class JobSchedulerScheduler implements Scheduler {
@RequiresApi(26)
@Override
public void schedule(long delay, @NonNull List constraints) {
- JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getNextId(), new ComponentName(application, SystemService.class))
+ JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
+ int currentId = getCurrentId();
+
+ if (constraints.isEmpty() && jobScheduler.getPendingJob(currentId) != null) {
+ Log.d(TAG, "Skipping JobScheduler enqueue because we have no constraints and there's already one pending.");
+ return;
+ }
+
+ JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getAndUpdateNextId(), new ComponentName(application, SystemService.class))
.setMinimumLatency(delay)
.setPersisted(true);
@@ -44,12 +51,15 @@ public class JobSchedulerScheduler implements Scheduler {
constraint.applyToJobInfo(jobInfoBuilder);
}
- Log.i(TAG, "Scheduling a run in " + delay + " ms.");
- JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
jobScheduler.schedule(jobInfoBuilder.build());
}
- private int getNextId() {
+ private int getCurrentId() {
+ SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ return prefs.getInt(PREF_NEXT_ID, 0);
+ }
+
+ private int getAndUpdateNextId() {
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
@@ -64,8 +74,6 @@ public class JobSchedulerScheduler implements Scheduler {
@Override
public boolean onStartJob(JobParameters params) {
- Log.d(TAG, "onStartJob()");
-
JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
@@ -73,7 +81,6 @@ public class JobSchedulerScheduler implements Scheduler {
public void onQueueEmpty() {
jobManager.removeOnEmptyQueueListener(this);
jobFinished(params, false);
- Log.d(TAG, "jobFinished()");
}
});
@@ -84,7 +91,6 @@ public class JobSchedulerScheduler implements Scheduler {
@Override
public boolean onStopJob(JobParameters params) {
- Log.d(TAG, "onStopJob()");
return false;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraint.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraint.java
new file mode 100644
index 000000000..40911c68b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraint.java
@@ -0,0 +1,43 @@
+package org.thoughtcrime.securesms.jobmanager.impl;
+
+import android.app.job.JobInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import org.thoughtcrime.securesms.jobmanager.Constraint;
+
+/**
+ * Job constraint for determining whether or not the device is actively charging.
+ */
+public class ChargingConstraint implements Constraint {
+
+ public static final String KEY = "ChargingConstraint";
+
+ private ChargingConstraint() {
+ }
+
+ @Override
+ public boolean isMet() {
+ return ChargingConstraintObserver.isCharging();
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @RequiresApi(26)
+ @Override
+ public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
+ jobInfoBuilder.setRequiresCharging(true);
+ }
+
+ public static final class Factory implements Constraint.Factory {
+
+ @Override
+ public ChargingConstraint create() {
+ return new ChargingConstraint();
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraintObserver.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraintObserver.java
new file mode 100644
index 000000000..4a478f3f8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/ChargingConstraintObserver.java
@@ -0,0 +1,62 @@
+package org.thoughtcrime.securesms.jobmanager.impl;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
+
+/**
+ * Observes the charging state of the device and notifies the JobManager system when appropriate.
+ */
+public class ChargingConstraintObserver implements ConstraintObserver {
+
+ private static final String REASON = ChargingConstraintObserver.class.getSimpleName();
+ private static final int STATUS_BATTERY = 0;
+
+ private final Application application;
+
+ private static volatile boolean charging;
+
+ public ChargingConstraintObserver(@NonNull Application application) {
+ this.application = application;
+ }
+
+ @Override
+ public void register(@NonNull Notifier notifier) {
+ Intent intent = application.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean wasCharging = charging;
+
+ charging = isCharging(intent);
+
+ if (charging && !wasCharging) {
+ notifier.onConstraintMet(REASON);
+ }
+ }
+ }, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+
+ charging = isCharging(intent);
+ }
+
+ public static boolean isCharging() {
+ return charging;
+ }
+
+ private static boolean isCharging(@Nullable Intent intent) {
+ if (intent == null) {
+ return false;
+ }
+
+ int status = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, STATUS_BATTERY);
+ return status != STATUS_BATTERY;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java
index 95a1996fc..62aacbef3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java
@@ -153,8 +153,8 @@ public final class AttachmentCompressionJob extends BaseJob {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, stripped, false);
- attachmentDatabase.markAttachmentAsTransformed(attachmentId);
}
+ attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, resized, false);
@@ -249,7 +249,7 @@ public final class AttachmentCompressionJob extends BaseJob {
try {
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context,
- new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()),
+ new DecryptableStreamUriLoader.DecryptableUri(attachment.getUri()),
constraints);
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
index ade4ad6ba..bcad898ff 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
@@ -1,14 +1,11 @@
package org.thoughtcrime.securesms.jobs;
import android.graphics.Bitmap;
-import android.media.MediaDataSource;
-import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
@@ -28,8 +25,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@@ -40,6 +35,7 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
@@ -157,8 +153,8 @@ public final class AttachmentUploadJob extends BaseJob {
private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException {
try {
- if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
- InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
+ if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
+ InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
@@ -193,43 +189,34 @@ public final class AttachmentUploadJob extends BaseJob {
private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException {
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
- if (attachment.getDataUri() == null) return null;
+ if (attachment.getUri() == null) return null;
- return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getDataUri()));
+ return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getUri()));
}
private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException {
- if (attachment.getThumbnailUri() != null) {
- return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getThumbnailUri()));
+ if (attachment.getBlurHash() != null) {
+ return attachment.getBlurHash().getHash();
}
- if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
-
if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported...");
return null;
}
- try (MediaDataSource dataSource = DatabaseFactory.getAttachmentDatabase(context).mediaDataSourceFor(attachmentId)) {
- if (dataSource == null) return null;
+ Bitmap bitmap = MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.getUri()), 1000);
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource);
+ if (bitmap != null) {
+ Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false);
+ bitmap.recycle();
- Bitmap bitmap = retriever.getFrameAtTime(1000);
+ Log.i(TAG, "Generated video thumbnail...");
+ String hash = BlurHashEncoder.encode(thumb);
+ thumb.recycle();
- if (bitmap != null) {
- Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false);
- bitmap.recycle();
-
- Log.i(TAG, "Generated video thumbnail...");
- String hash = BlurHashEncoder.encode(thumb);
- thumb.recycle();
-
- return hash;
- } else {
- return null;
- }
+ return hash;
+ } else {
+ return null;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index 5e51c4b63..7409d6856 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -10,6 +10,8 @@ import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobMigration;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
+import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
+import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
@@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMi
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
+import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
@@ -124,6 +127,7 @@ public final class JobManagerFactories {
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
// Migrations
+ put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
@@ -158,6 +162,7 @@ public final class JobManagerFactories {
public static Map getConstraintFactories(@NonNull Application application) {
return new HashMap() {{
+ put(ChargingConstraint.KEY, new ChargingConstraint.Factory());
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application));
@@ -168,6 +173,7 @@ public final class JobManagerFactories {
public static List getConstraintObservers(@NonNull Application application) {
return Arrays.asList(CellServiceConstraintObserver.getInstance(application),
+ new ChargingConstraintObserver(application),
new NetworkConstraintObserver(application),
new SqlCipherMigrationConstraintObserver(),
new WebsocketDrainedConstraintObserver());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java
index 4de6f95ac..f189ba5f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java
@@ -134,7 +134,7 @@ public class LeaveGroupJob extends BaseJob {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddresses(context, destinations);
List memberAddresses = RecipientUtil.toSignalServiceAddresses(context, members);
- List> unidentifiedAccess = Stream.of(destinations).map(Recipient::resolved).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, Stream.of(destinations).map(Recipient::resolved).toList());
SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId.getDecodedId(), name, memberAddresses, null);
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java
index d203fe35f..22f35f625 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -27,18 +28,30 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
-public class LocalBackupJob extends BaseJob {
+public final class LocalBackupJob extends BaseJob {
public static final String KEY = "LocalBackupJob";
- private static final String TAG = LocalBackupJob.class.getSimpleName();
+ private static final String TAG = Log.tag(LocalBackupJob.class);
- public LocalBackupJob() {
- this(new Job.Parameters.Builder()
- .setQueue("__LOCAL_BACKUP__")
- .setMaxInstances(1)
- .setMaxAttempts(3)
- .build());
+ public static final String TEMP_BACKUP_FILE_PREFIX = ".backup";
+ public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp";
+
+ public LocalBackupJob(boolean forceNow) {
+ this(buildParameters(forceNow));
+ }
+
+ private static @NonNull Job.Parameters buildParameters(boolean forceNow) {
+ Job.Parameters.Builder builder = new Job.Parameters.Builder()
+ .setQueue("__LOCAL_BACKUP__")
+ .setMaxInstances(1)
+ .setMaxAttempts(3);
+
+ if (!forceNow) {
+ builder.addConstraint(ChargingConstraint.KEY);
+ }
+
+ return builder.build();
}
private LocalBackupJob(@NonNull Job.Parameters parameters) {
@@ -76,6 +89,8 @@ public class LocalBackupJob extends BaseJob {
String fileName = String.format("signal-%s.backup", timestamp);
File backupFile = new File(backupDirectory, fileName);
+ deleteOldTemporaryBackups(backupDirectory);
+
if (backupFile.exists()) {
throw new IOException("Backup file already exists?");
}
@@ -84,7 +99,7 @@ public class LocalBackupJob extends BaseJob {
throw new IOException("Backup password is null");
}
- File tempFile = File.createTempFile("backup", "tmp", StorageUtil.getBackupCacheDirectory(context));
+ File tempFile = File.createTempFile(TEMP_BACKUP_FILE_PREFIX, TEMP_BACKUP_FILE_SUFFIX, backupDirectory);
try {
FullBackupExporter.export(context,
@@ -111,6 +126,21 @@ public class LocalBackupJob extends BaseJob {
}
}
+ private static void deleteOldTemporaryBackups(@NonNull File backupDirectory) {
+ for (File file : backupDirectory.listFiles()) {
+ if (file.isFile()) {
+ String name = file.getName();
+ if (name.startsWith(TEMP_BACKUP_FILE_PREFIX) && name.endsWith(TEMP_BACKUP_FILE_SUFFIX)) {
+ if (file.delete()) {
+ Log.w(TAG, "Deleted old temporary backup file");
+ } else {
+ Log.w(TAG, "Could not delete old temporary backup file");
+ }
+ }
+ }
+ }
+ }
+
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return false;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 8941ac790..f0a7d24b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -13,6 +13,9 @@ import com.google.android.mms.pdu_alt.RetrieveConf;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
+import org.thoughtcrime.securesms.contactshare.Contact;
+import org.thoughtcrime.securesms.contactshare.ContactUtil;
+import org.thoughtcrime.securesms.contactshare.VCardUtil;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
@@ -33,6 +36,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.KeyCachingService;
+import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -189,6 +193,7 @@ public class MmsDownloadJob extends BaseJob {
Set members = new HashSet<>();
String body = null;
List attachments = new LinkedList<>();
+ List sharedContacts = new LinkedList<>();
RecipientId from = null;
@@ -223,14 +228,18 @@ public class MmsDownloadJob extends BaseJob {
PduPart part = media.getPart(i);
if (part.getData() != null) {
- Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
- String name = null;
+ if (Util.toIsoString(part.getContentType()).toLowerCase().equals(MediaUtil.VCARD)){
+ sharedContacts.addAll(VCardUtil.parseContacts(new String(part.getData())));
+ } else {
+ Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
+ String name = null;
- if (part.getName() != null) name = Util.toIsoString(part.getName());
+ if (part.getName() != null) name = Util.toIsoString(part.getName());
- attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
- AttachmentDatabase.TRANSFER_PROGRESS_DONE,
- part.getData().length, name, false, false, false, null, null, null, null, null));
+ attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
+ AttachmentDatabase.TRANSFER_PROGRESS_DONE,
+ part.getData().length, name, false, false, false, null, null, null, null, null));
+ }
}
}
}
@@ -240,7 +249,7 @@ public class MmsDownloadJob extends BaseJob {
group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients));
}
- IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false);
+ IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts));
Optional insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java
index a4eb99fb1..2fabd66b6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java
@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase;
-import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -273,7 +272,7 @@ public final class MmsSendJob extends SendJob {
for (Attachment attachment : scaledAttachments) {
try {
- if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
+ if (attachment.getUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
String fileName = attachment.getFileName();
PduPart part = new PduPart();
@@ -296,7 +295,7 @@ public final class MmsSendJob extends SendJob {
int index = fileName.lastIndexOf(".");
String contentId = (index == -1) ? fileName : fileName.substring(0, index);
part.setContentId(contentId.getBytes());
- part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, attachment.getDataUri())));
+ part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, attachment.getUri())));
body.addPart(part);
size += getPartSize(part);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
index ef97cca0c..946007159 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
@@ -261,14 +261,11 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
{
if (length > 0) {
try {
- SignalServiceAttachmentStream.Builder attachmentStream = SignalServiceAttachment.newStreamBuilder()
- .withStream(stream)
- .withContentType("application/octet-stream")
- .withLength(length);
-
- if (FeatureFlags.attachmentsV3()) {
- attachmentStream.withResumableUploadSpec(messageSender.getResumableUploadSpec());
- }
+ SignalServiceAttachmentStream.Builder attachmentStream = SignalServiceAttachment.newStreamBuilder()
+ .withStream(stream)
+ .withContentType("application/octet-stream")
+ .withLength(length)
+ .withResumableUploadSpec(messageSender.getResumableUploadSpec());
messageSender.sendMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)),
UnidentifiedAccessUtil.getAccessForSync(context));
@@ -328,7 +325,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
.withLength(fd.getLength())
.build());
} catch (IOException e) {
- Log.i(TAG, "Could not find avatar for URI: " + displayPhotoUri);
+ // Ignored
}
Uri photoUri = Uri.withAppendedPath(uri, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java
index 66fa83e7d..fba1353f4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java
@@ -125,7 +125,7 @@ public class ProfileKeySendJob extends BaseJob {
private List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations) throws IOException, UntrustedIdentityException {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
- List> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.asProfileKeyUpdate(true)
.withTimestamp(System.currentTimeMillis())
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index 7eee96ab7..d549ba51e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -303,9 +303,7 @@ public final class PushGroupSendJob extends PushSendJob {
boolean isRecipientUpdate = Stream.of(DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId))
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
- List> unidentifiedAccess = Stream.of(destinations)
- .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
- .toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
if (message.isGroup()) {
OutgoingGroupUpdateMessage groupMessage = (OutgoingGroupUpdateMessage) message;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java
index 111131035..ad7b154ad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java
@@ -78,13 +78,7 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
.filter(uuid -> !UuidUtil.UNKNOWN_UUID.equals(uuid))
.filter(uuid -> !Recipient.self().getUuid().get().equals(uuid))
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
- .filter(recipient -> {
- if (FeatureFlags.cds()) {
- return recipient.getRegistered() != RecipientDatabase.RegisteredState.NOT_REGISTERED;
- } else {
- return true;
- }
- })
+ .filter(recipient -> recipient.getRegistered() != RecipientDatabase.RegisteredState.NOT_REGISTERED)
.map(Recipient::getId)
.collect(Collectors.toSet());
@@ -165,7 +159,7 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
- List> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
SignalServiceGroupV2 group = SignalServiceGroupV2.fromProtobuf(groupContextV2);
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
index c093e039b..080f0c680 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
@@ -82,7 +82,6 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.Base64;
-import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -503,12 +502,15 @@ public final class PushProcessMessageJob extends BaseJob {
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsMissedCall(smsMessageId.get());
} else {
- Intent intent = new Intent(context, WebRtcCallService.class);
- RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
+ Intent intent = new Intent(context, WebRtcCallService.class);
+ Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
+ RemotePeer remotePeer = new RemotePeer(recipient.getId());
+ byte[] remoteIdentityKey = recipient.getIdentityKey();
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
+ .putExtra(WebRtcCallService.EXTRA_REMOTE_IDENTITY_KEY, remoteIdentityKey)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_OFFER_OPAQUE, message.getOpaque())
.putExtra(WebRtcCallService.EXTRA_OFFER_SDP, message.getSdp())
@@ -526,16 +528,19 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull AnswerMessage message)
{
Log.i(TAG, "handleCallAnswerMessage...");
- Intent intent = new Intent(context, WebRtcCallService.class);
- RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
+ Intent intent = new Intent(context, WebRtcCallService.class);
+ Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
+ RemotePeer remotePeer = new RemotePeer(recipient.getId());
+ byte[] remoteIdentityKey = recipient.getIdentityKey();
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ANSWER)
- .putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
- .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
- .putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
- .putExtra(WebRtcCallService.EXTRA_ANSWER_OPAQUE, message.getOpaque())
- .putExtra(WebRtcCallService.EXTRA_ANSWER_SDP, message.getSdp())
- .putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
+ .putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
+ .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
+ .putExtra(WebRtcCallService.EXTRA_REMOTE_IDENTITY_KEY, remoteIdentityKey)
+ .putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
+ .putExtra(WebRtcCallService.EXTRA_ANSWER_OPAQUE, message.getOpaque())
+ .putExtra(WebRtcCallService.EXTRA_ANSWER_SDP, message.getSdp())
+ .putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
context.startService(intent);
}
@@ -1667,7 +1672,6 @@ public final class PushProcessMessageJob extends BaseJob {
if (stickerRecord != null) {
return Optional.of(new UriAttachment(stickerRecord.getUri(),
- stickerRecord.getUri(),
stickerRecord.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
stickerRecord.getSize(),
@@ -1817,12 +1821,6 @@ public final class PushProcessMessageJob extends BaseJob {
} else if (conversation.isGroup()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
- boolean isGv2Message = message.isGroupV2Message();
-
- if (isGv2Message && !FeatureFlags.groupsV2() && groupDatabase.isUnknownGroup(groupId.get())) {
- Log.i(TAG, "Ignoring GV2 message for a new group by feature flag.");
- return true;
- }
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
return false;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 8dd0cc68a..06e78a484 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.graphics.Bitmap;
+import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
-import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -118,8 +118,8 @@ public abstract class PushSendJob extends SendJob {
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
try {
- if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
- InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
+ if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
+ InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
return SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
@@ -192,14 +192,26 @@ public abstract class PushSendJob extends SendJob {
final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation());
final byte[] key = Base64.decode(attachment.getKey());
+ int width = attachment.getWidth();
+ int height = attachment.getHeight();
+
+ if ((width == 0 || height == 0) && MediaUtil.hasVideoThumbnail(context, attachment.getUri())) {
+ Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000);
+
+ if (thumbnail != null) {
+ width = thumbnail.getWidth();
+ height = thumbnail.getHeight();
+ }
+ }
+
return new SignalServiceAttachmentPointer(attachment.getCdnNumber(),
remoteId,
attachment.getContentType(),
key,
Optional.of(Util.toIntExact(attachment.getSize())),
Optional.absent(),
- attachment.getWidth(),
- attachment.getHeight(),
+ width,
+ height,
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
@@ -240,13 +252,17 @@ public abstract class PushSendJob extends SendJob {
String thumbnailType = MediaUtil.IMAGE_JPEG;
try {
- if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
+ if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getUri() != null) {
Bitmap.CompressFormat format = BitmapUtil.getCompressFormatForContentType(attachment.getContentType());
- thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024, format);
+ thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getUri()), 100, 100, 500 * 1024, format);
thumbnailType = attachment.getContentType();
- } else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
- thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
+ } else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.getContentType()) && attachment.getUri() != null) {
+ Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000);
+
+ if (bitmap != null) {
+ thumbnailData = BitmapUtil.createScaledBytes(context, bitmap, 100, 100, 500 * 1024);
+ }
}
if (thumbnailData != null) {
@@ -255,11 +271,8 @@ public abstract class PushSendJob extends SendJob {
.withWidth(thumbnailData.getWidth())
.withHeight(thumbnailData.getHeight())
.withLength(thumbnailData.getBitmap().length)
- .withStream(new ByteArrayInputStream(thumbnailData.getBitmap()));
-
- if (FeatureFlags.attachmentsV3()) {
- builder.withResumableUploadSpec(ApplicationDependencies.getSignalServiceMessageSender().getResumableUploadSpec());
- }
+ .withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
+ .withResumableUploadSpec(ApplicationDependencies.getSignalServiceMessageSender().getResumableUploadSpec());
thumbnail = builder.build();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java
index 54edd286b..805d523b3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java
@@ -210,7 +210,7 @@ public class ReactionSendJob extends BaseJob {
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
- List> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
index 553f40e48..2af0325c4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
@@ -16,8 +16,8 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
+import org.whispersystems.signalservice.api.account.AccountAttributes;
import java.io.IOException;
@@ -73,7 +73,7 @@ public class RefreshAttributesJob extends BaseJob {
boolean phoneNumberDiscoverable = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable();
- SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut());
+ AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut());
Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() +
"\n Phone number discoverable : " + phoneNumberDiscoverable +
"\n Capabilities:" +
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java
index 48da0f282..cc33226d8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java
@@ -166,7 +166,7 @@ public class RemoteDeleteSendJob extends BaseJob {
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
- List> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java
index 5db8c7a29..128c537f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java
@@ -35,11 +35,6 @@ public class ResumableUploadSpecJob extends BaseJob {
@Override
protected void onRun() throws Exception {
- if (!FeatureFlags.attachmentsV3()) {
- Log.i(TAG, "Attachments V3 is not enabled so there is nothing to do!");
- return;
- }
-
ResumableUploadSpec resumableUploadSpec = ApplicationDependencies.getSignalServiceMessageSender()
.getResumableUploadSpec();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
index 944b9d771..0b60652c2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -54,7 +55,9 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -178,6 +181,8 @@ public class RetrieveProfileJob extends BaseJob {
current - TimeUnit.DAYS.toMillis(1),
50);
+ ids.add(Recipient.self().getId());
+
if (ids.size() > 0) {
Log.i(TAG, "Optimistically refreshing " + ids.size() + " eligible recipient(s).");
enqueue(new HashSet<>(ids));
@@ -218,11 +223,17 @@ public class RetrieveProfileJob extends BaseJob {
@Override
public void onRun() throws IOException, RetryLaterException {
- Stopwatch stopwatch = new Stopwatch("RetrieveProfile");
- Set retries = new HashSet<>();
+ Stopwatch stopwatch = new Stopwatch("RetrieveProfile");
+ RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
+ Set retries = new HashSet<>();
+ Set unregistered = new HashSet<>();
- List recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
- stopwatch.split("resolve");
+ RecipientUtil.ensureUuidsAreAvailable(context, Stream.of(Recipient.resolvedList(recipientIds))
+ .filter(r -> r.getRegistered() != RecipientDatabase.RegisteredState.NOT_REGISTERED)
+ .toList());
+
+ List recipients = Recipient.resolvedList(recipientIds);
+ stopwatch.split("resolve-ensure");
List>> futures = Stream.of(recipients)
.filter(Recipient::hasServiceIdentifier)
@@ -244,6 +255,9 @@ public class RetrieveProfileJob extends BaseJob {
retries.add(recipient.getId());
} else if (e.getCause() instanceof NotFoundException) {
Log.w(TAG, "Failed to find a profile for " + recipient.getId());
+ if (recipient.isRegistered()) {
+ unregistered.add(recipient.getId());
+ }
} else {
Log.w(TAG, "Failed to retrieve profile for " + recipient.getId());
}
@@ -259,7 +273,18 @@ public class RetrieveProfileJob extends BaseJob {
}
Set success = SetUtil.difference(recipientIds, retries);
- DatabaseFactory.getRecipientDatabase(context).markProfilesFetched(success, System.currentTimeMillis());
+ recipientDatabase.markProfilesFetched(success, System.currentTimeMillis());
+
+ Map newlyRegistered = Stream.of(profiles)
+ .map(Pair::first)
+ .filterNot(Recipient::isRegistered)
+ .collect(Collectors.toMap(Recipient::getId,
+ r -> r.getUuid().transform(UUID::toString).orNull()));
+
+ if (unregistered.size() > 0 || newlyRegistered.size() > 0) {
+ Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + unregistered.size() + " users as unregistered.");
+ recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, unregistered);
+ }
stopwatch.split("process");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java
index c0190587e..6c9b9a529 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java
@@ -2,9 +2,19 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.Person;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
import android.telephony.SmsMessage;
-import org.thoughtcrime.securesms.database.Database;
+import com.google.android.gms.auth.api.phone.SmsRetriever;
+import com.google.android.gms.common.api.Status;
+
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
@@ -14,16 +24,21 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
-import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.notifications.NotificationChannels;
+import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.service.VerificationCodeParser;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
+import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.Base64;
+import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
public class SmsReceiveJob extends BaseJob {
@@ -41,7 +56,7 @@ public class SmsReceiveJob extends BaseJob {
public SmsReceiveJob(@Nullable Object[] pdus, int subscriptionId) {
this(new Job.Parameters.Builder()
.addConstraint(SqlCipherMigrationConstraint.KEY)
- .setMaxAttempts(25)
+ .setLifespan(TimeUnit.DAYS.toMillis(1))
.build(),
pdus,
subscriptionId);
@@ -72,13 +87,36 @@ public class SmsReceiveJob extends BaseJob {
}
@Override
- public void onRun() throws MigrationPendingException {
- if (TextSecurePreferences.getLocalUuid(context) == null && TextSecurePreferences.getLocalNumber(context) == null) {
- throw new NotReadyException();
- }
-
+ public void onRun() throws MigrationPendingException, RetryLaterException {
Optional message = assembleMessageFragments(pdus, subscriptionId);
+ if (TextSecurePreferences.getLocalUuid(context) == null && TextSecurePreferences.getLocalNumber(context) == null) {
+ Log.i(TAG, "Received an SMS before we're registered...");
+
+ if (message.isPresent()) {
+ Optional token = VerificationCodeParser.parse(message.get().getMessageBody());
+
+ if (token.isPresent()) {
+ Log.i(TAG, "Received something that looks like a registration SMS. Posting a notification and broadcast.");
+
+ NotificationManager manager = ServiceUtil.getNotificationManager(context);
+ Notification notification = buildPreRegistrationNotification(context, message.get());
+ manager.notify(NotificationIds.PRE_REGISTRATION_SMS, notification);
+
+ Intent smsRetrieverIntent = buildSmsRetrieverIntent(message.get());
+ context.sendBroadcast(smsRetrieverIntent);
+
+ return;
+ } else {
+ Log.w(TAG, "Received an SMS before registration is complete. We'll try again later.");
+ throw new RetryLaterException();
+ }
+ } else {
+ Log.w(TAG, "Received an SMS before registration is complete, but couldn't assemble the message anyway. Ignoring.");
+ return;
+ }
+ }
+
if (message.isPresent() && !isBlocked(message.get())) {
Optional insertResult = storeMessage(message.get());
@@ -99,7 +137,8 @@ public class SmsReceiveJob extends BaseJob {
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
- return exception instanceof MigrationPendingException;
+ return exception instanceof MigrationPendingException ||
+ exception instanceof RetryLaterException;
}
private boolean isBlocked(IncomingTextMessage message) {
@@ -150,7 +189,29 @@ public class SmsReceiveJob extends BaseJob {
return Optional.of(new IncomingTextMessage(messages));
}
- private class MigrationPendingException extends Exception {
+ private static Notification buildPreRegistrationNotification(@NonNull Context context, @NonNull IncomingTextMessage message) {
+ Recipient sender = Recipient.resolved(message.getSender());
+
+ return new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
+ .setStyle(new NotificationCompat.MessagingStyle(new Person.Builder()
+ .setName(sender.getE164().or(""))
+ .build())
+ .addMessage(new NotificationCompat.MessagingStyle.Message(message.getMessageBody(),
+ message.getSentTimestampMillis(),
+ (Person) null)))
+ .setSmallIcon(R.drawable.ic_notification)
+ .build();
+ }
+
+ /**
+ * @return An intent that is identical to the one the {@link SmsRetriever} API uses, so that
+ * we can auto-populate the SMS code on capable devices.
+ */
+ private static Intent buildSmsRetrieverIntent(@NonNull IncomingTextMessage message) {
+ Intent intent = new Intent(SmsRetriever.SMS_RETRIEVED_ACTION);
+ intent.putExtra(SmsRetriever.EXTRA_STATUS, Status.RESULT_SUCCESS);
+ intent.putExtra(SmsRetriever.EXTRA_SMS_MESSAGE, message.getMessageBody());
+ return intent;
}
public static final class Factory implements Job.Factory {
@@ -172,6 +233,6 @@ public class SmsReceiveJob extends BaseJob {
}
}
- private static class NotReadyException extends RuntimeException {
+ private class MigrationPendingException extends Exception {
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java
index b36e86aa1..c951e79f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java
@@ -112,7 +112,7 @@ public class TypingSendJob extends BaseJob {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
- List> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList();
+ List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients);
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
if (isCanceled()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java
index 6c505fccb..136fb581a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java
@@ -335,7 +335,6 @@ public class LinkPreviewRepository {
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
return Optional.of(new UriAttachment(uri,
- uri,
contentType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
bytes.length,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java
index 4b47c4cf1..7ad8e6d68 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java
@@ -148,7 +148,7 @@ public final class LinkPreviewUtil {
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
- htmlTitle = titleMatcher.group(1);
+ htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
}
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java
index 9c49dc37a..ec7329c35 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.AppCapabilities;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class LogSectionCapabilities implements LogSection {
@@ -28,7 +28,7 @@ public final class LogSectionCapabilities implements LogSection {
Recipient self = Recipient.self();
- SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(false);
+ AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(false);
return new StringBuilder().append("Local device UUID : ").append(capabilities.isUuid()).append("\n")
.append("Global UUID : ").append(self.getUuidCapability()).append("\n")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java
index 873da276b..e147f793c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
+import org.thoughtcrime.securesms.BuildConfig;
import org.whispersystems.libsignal.util.Pair;
import java.util.ArrayList;
@@ -24,7 +25,7 @@ public class LogSectionPermissions implements LogSection {
List> status = new ArrayList<>();
try {
- PackageInfo info = context.getPackageManager().getPackageInfo("org.thoughtcrime.securesms", PackageManager.GET_PERMISSIONS);
+ PackageInfo info = context.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_PERMISSIONS);
for (int i = 0; i < info.requestedPermissions.length; i++) {
status.add(new Pair<>(info.requestedPermissions[i],
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
index 23ed5769a..c32cada08 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
@@ -47,8 +47,8 @@ final class MediaActions {
List attachments = new LinkedList<>();
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
- if (mediaRecord.getAttachment().getDataUri() != null) {
- attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
+ if (mediaRecord.getAttachment().getUri() != null) {
+ attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getUri(),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.getAttachment().getFileName()));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
index 3c7e6371f..c07ef1d2a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
@@ -210,7 +210,7 @@ public final class MediaOverviewPageFragment extends Fragment
}
private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
- if (mediaRecord.getAttachment().getDataUri() == null) {
+ if (mediaRecord.getAttachment().getUri() == null) {
return;
}
@@ -231,7 +231,7 @@ public final class MediaOverviewPageFragment extends Fragment
intent.putExtra(MediaPreviewActivity.SHOW_THREAD_EXTRA, threadId == MediaDatabase.ALL_THREADS);
intent.putExtra(MediaPreviewActivity.SORTING_EXTRA, sorting.ordinal());
- intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
+ intent.setDataAndType(mediaRecord.getAttachment().getUri(), mediaRecord.getContentType());
context.startActivity(intent);
} else {
if (!MediaUtil.isAudio(attachment)) {
@@ -241,7 +241,7 @@ public final class MediaOverviewPageFragment extends Fragment
}
private static void showFileExternally(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord) {
- Uri uri = mediaRecord.getAttachment().getDataUri();
+ Uri uri = mediaRecord.getAttachment().getUri();
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java
index 16e213998..8962e7360 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java
@@ -29,7 +29,7 @@ public abstract class MediaPreviewFragment extends Fragment {
protected Events events;
public static MediaPreviewFragment newInstance(@NonNull Attachment attachment, boolean autoPlay) {
- return newInstance(attachment.getDataUri(), attachment.getContentType(), attachment.getSize(), autoPlay);
+ return newInstance(attachment.getUri(), attachment.getContentType(), attachment.getSize(), autoPlay);
}
public static MediaPreviewFragment newInstance(@NonNull Uri dataUri, @NonNull String contentType, long size, boolean autoPlay) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
index d32a490c0..322542eb6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
@@ -98,8 +98,7 @@ public class MediaPreviewViewModel extends ViewModel {
}
private @Nullable Media toMedia(@NonNull MediaRecord mediaRecord) {
- Uri uri = mediaRecord.getAttachment().getThumbnailUri() != null ? mediaRecord.getAttachment().getThumbnailUri()
- : mediaRecord.getAttachment().getDataUri();
+ Uri uri = mediaRecord.getAttachment().getUri();
if (uri == null) {
return null;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java
index 94e5c198f..5f87a7776 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java
@@ -191,11 +191,11 @@ class MediaUploadRepository {
public static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
if (MediaUtil.isVideoType(media.getMimeType())) {
- return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment();
+ return new VideoSlide(context, media.getUri(), media.getSize(), media.getWidth(), media.getHeight(), media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment();
} else if (MediaUtil.isGif(media.getMimeType())) {
- return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull()).asAttachment();
+ return new GifSlide(context, media.getUri(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull()).asAttachment();
} else if (MediaUtil.isImageType(media.getMimeType())) {
- return new ImageSlide(context, media.getUri(), media.getMimeType(), 0, media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull(), null).asAttachment();
+ return new ImageSlide(context, media.getUri(), media.getMimeType(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull(), null).asAttachment();
} else if (MediaUtil.isTextType(media.getMimeType())) {
return new TextSlide(context, media.getUri(), null, media.getSize()).asAttachment();
} else {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlacklist.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlacklist.java
index 35b6dd789..2e901014f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlacklist.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlacklist.java
@@ -8,8 +8,10 @@ import java.util.Set;
public final class CameraXModelBlacklist {
private static final Set BLACKLIST = new HashSet() {{
// Pixel 4
- add("Pixel 4");
- add("Pixel 4 XL");
+ if (Build.VERSION.SDK_INT < 30) {
+ add("Pixel 4");
+ add("Pixel 4 XL");
+ }
// Huawei Mate 10
add("ALP-L29");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java
index ba13b7098..0de9510e6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java
@@ -14,11 +14,11 @@ import org.thoughtcrime.securesms.R;
public class BasicMegaphoneView extends FrameLayout {
- private ImageView image;
- private TextView titleText;
- private TextView bodyText;
- private Button actionButton;
- private Button snoozeButton;
+ private ImageView image;
+ private TextView titleText;
+ private TextView bodyText;
+ private Button actionButton;
+ private Button secondaryButton;
private Megaphone megaphone;
private MegaphoneActionController megaphoneListener;
@@ -36,11 +36,11 @@ public class BasicMegaphoneView extends FrameLayout {
private void init(@NonNull Context context) {
inflate(context, R.layout.basic_megaphone_view, this);
- this.image = findViewById(R.id.basic_megaphone_image);
- this.titleText = findViewById(R.id.basic_megaphone_title);
- this.bodyText = findViewById(R.id.basic_megaphone_body);
- this.actionButton = findViewById(R.id.basic_megaphone_action);
- this.snoozeButton = findViewById(R.id.basic_megaphone_snooze);
+ this.image = findViewById(R.id.basic_megaphone_image);
+ this.titleText = findViewById(R.id.basic_megaphone_title);
+ this.bodyText = findViewById(R.id.basic_megaphone_body);
+ this.actionButton = findViewById(R.id.basic_megaphone_action);
+ this.secondaryButton = findViewById(R.id.basic_megaphone_secondary);
}
@Override
@@ -89,17 +89,27 @@ public class BasicMegaphoneView extends FrameLayout {
actionButton.setVisibility(GONE);
}
- if (megaphone.canSnooze()) {
- snoozeButton.setVisibility(VISIBLE);
- snoozeButton.setOnClickListener(v -> {
- megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
+ if (megaphone.canSnooze() || megaphone.hasSecondaryButton()) {
+ secondaryButton.setVisibility(VISIBLE);
- if (megaphone.getSnoozeListener() != null) {
- megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
- }
- });
+ if (megaphone.canSnooze()) {
+ secondaryButton.setOnClickListener(v -> {
+ megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
+
+ if (megaphone.getSnoozeListener() != null) {
+ megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
+ }
+ });
+ } else {
+ secondaryButton.setText(megaphone.getSecondaryButtonText());
+ secondaryButton.setOnClickListener(v -> {
+ if (megaphone.getSecondaryButtonClickListener() != null) {
+ megaphone.getSecondaryButtonClickListener().onEvent(megaphone, megaphoneListener);
+ }
+ });
+ }
} else {
- snoozeButton.setVisibility(GONE);
+ secondaryButton.setVisibility(GONE);
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
index a001cb3e3..da90fd9e9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
@@ -28,20 +28,24 @@ public class Megaphone {
private final int buttonTextRes;
private final EventListener buttonListener;
private final EventListener snoozeListener;
+ private final int secondaryButtonTextRes;
+ private final EventListener secondaryButtonListener;
private final EventListener onVisibleListener;
private Megaphone(@NonNull Builder builder) {
- this.event = builder.event;
- this.style = builder.style;
- this.priority = builder.priority;
- this.canSnooze = builder.canSnooze;
- this.titleRes = builder.titleRes;
- this.bodyRes = builder.bodyRes;
- this.imageRequest = builder.imageRequest;
- this.buttonTextRes = builder.buttonTextRes;
- this.buttonListener = builder.buttonListener;
- this.snoozeListener = builder.snoozeListener;
- this.onVisibleListener = builder.onVisibleListener;
+ this.event = builder.event;
+ this.style = builder.style;
+ this.priority = builder.priority;
+ this.canSnooze = builder.canSnooze;
+ this.titleRes = builder.titleRes;
+ this.bodyRes = builder.bodyRes;
+ this.imageRequest = builder.imageRequest;
+ this.buttonTextRes = builder.buttonTextRes;
+ this.buttonListener = builder.buttonListener;
+ this.snoozeListener = builder.snoozeListener;
+ this.secondaryButtonTextRes = builder.secondaryButtonTextRes;
+ this.secondaryButtonListener = builder.secondaryButtonListener;
+ this.onVisibleListener = builder.onVisibleListener;
}
public @NonNull Event getEvent() {
@@ -88,6 +92,18 @@ public class Megaphone {
return snoozeListener;
}
+ public @StringRes int getSecondaryButtonText() {
+ return secondaryButtonTextRes;
+ }
+
+ public boolean hasSecondaryButton() {
+ return secondaryButtonTextRes != 0;
+ }
+
+ public @Nullable EventListener getSecondaryButtonClickListener() {
+ return secondaryButtonListener;
+ }
+
public @Nullable EventListener getOnVisibleListener() {
return onVisibleListener;
}
@@ -105,6 +121,8 @@ public class Megaphone {
private int buttonTextRes;
private EventListener buttonListener;
private EventListener snoozeListener;
+ private int secondaryButtonTextRes;
+ private EventListener secondaryButtonListener;
private EventListener onVisibleListener;
@@ -159,6 +177,12 @@ public class Megaphone {
return this;
}
+ public @NonNull Builder setSecondaryButton(@StringRes int secondaryButtonTextRes, @NonNull EventListener listener) {
+ this.secondaryButtonTextRes = secondaryButtonTextRes;
+ this.secondaryButtonListener = listener;
+ return this;
+ }
+
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
this.onVisibleListener = listener;
return this;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java
index 6aea1a9cb..66fdb31fc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
+import androidx.fragment.app.DialogFragment;
public interface MegaphoneActionController {
/**
@@ -36,4 +37,9 @@ public interface MegaphoneActionController {
* Called when a megaphone completed its goal.
*/
void onMegaphoneCompleted(@NonNull Megaphones.Event event);
+
+ /**
+ * When a megaphone wnats to show a dialog fragment.
+ */
+ void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
index a0d4c73c8..c3e9fbcab 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import androidx.annotation.AnyThread;
-import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -53,7 +52,8 @@ public class MegaphoneRepository {
executor.execute(() -> {
database.markFinished(Event.REACTIONS);
database.markFinished(Event.MESSAGE_REQUESTS);
- database.markFinished(Event.MENTIONS);
+ database.markFinished(Event.LINK_PREVIEWS);
+ database.markFinished(Event.RESEARCH);
resetDatabaseCache();
});
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
index e792b5a34..e9c0fc832 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivit
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
+import org.thoughtcrime.securesms.util.ResearchMegaphone;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.LinkedHashMap;
@@ -85,9 +86,9 @@ public final class Megaphones {
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
- put(Event.MENTIONS, shouldShowMentionsMegaphone() ? ALWAYS : NEVER);
put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER);
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
+ put(Event.RESEARCH, shouldShowResearchMegaphone() ? ShowForDurationSchedule.showForDays(7) : NEVER);
}};
}
@@ -101,12 +102,12 @@ public final class Megaphones {
return buildPinReminderMegaphone(context);
case MESSAGE_REQUESTS:
return buildMessageRequestsMegaphone(context);
- case MENTIONS:
- return buildMentionsMegaphone();
case LINK_PREVIEWS:
return buildLinkPreviewsMegaphone();
case CLIENT_DEPRECATED:
return buildClientDeprecatedMegaphone(context);
+ case RESEARCH:
+ return buildResearchMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -189,14 +190,6 @@ public final class Megaphones {
.build();
}
- private static Megaphone buildMentionsMegaphone() {
- return new Megaphone.Builder(Event.MENTIONS, Megaphone.Style.POPUP)
- .setTitle(R.string.MentionsMegaphone__introducing_mentions)
- .setBody(R.string.MentionsMegaphone__get_someones_attention_in_a_group_by_typing)
- .setImage(R.drawable.mention_megaphone)
- .build();
- }
-
private static @NonNull Megaphone buildLinkPreviewsMegaphone() {
return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS)
.setPriority(Megaphone.Priority.HIGH)
@@ -207,9 +200,22 @@ public final class Megaphones {
return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN)
.disableSnooze()
.setPriority(Megaphone.Priority.HIGH)
- .setOnVisibleListener((megaphone, listener) -> {
- listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class));
+ .setOnVisibleListener((megaphone, listener) -> listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class)))
+ .build();
+ }
+
+ private static @NonNull Megaphone buildResearchMegaphone(@NonNull Context context) {
+ return new Megaphone.Builder(Event.RESEARCH, Megaphone.Style.BASIC)
+ .disableSnooze()
+ .setTitle(R.string.ResearchMegaphone_tell_signal_what_you_think)
+ .setBody(R.string.ResearchMegaphone_to_make_signal_the_best_messaging_app_on_the_planet)
+ .setImage(R.drawable.ic_research_megaphone)
+ .setActionButton(R.string.ResearchMegaphone_learn_more, (megaphone, controller) -> {
+ controller.onMegaphoneCompleted(megaphone.getEvent());
+ controller.onMegaphoneDialogFragmentRequested(new ResearchMegaphoneDialog());
})
+ .setSecondaryButton(R.string.ResearchMegaphone_dismiss, (megaphone, controller) -> controller.onMegaphoneCompleted(megaphone.getEvent()))
+ .setPriority(Megaphone.Priority.DEFAULT)
.build();
}
@@ -217,9 +223,8 @@ public final class Megaphones {
return Recipient.self().getProfileName() == ProfileName.EMPTY;
}
- private static boolean shouldShowMentionsMegaphone() {
- return false;
-// return FeatureFlags.mentions();
+ private static boolean shouldShowResearchMegaphone() {
+ return ResearchMegaphone.isInResearchMegaphone();
}
private static boolean shouldShowLinkPreviewsMegaphone(@NonNull Context context) {
@@ -231,9 +236,9 @@ public final class Megaphones {
PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder"),
MESSAGE_REQUESTS("message_requests"),
- MENTIONS("mentions"),
LINK_PREVIEWS("link_previews"),
- CLIENT_DEPRECATED("client_deprecated");
+ CLIENT_DEPRECATED("client_deprecated"),
+ RESEARCH("research");
private final String key;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/ResearchMegaphoneDialog.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ResearchMegaphoneDialog.java
new file mode 100644
index 000000000..4b190deb8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ResearchMegaphoneDialog.java
@@ -0,0 +1,47 @@
+package org.thoughtcrime.securesms.megaphone;
+
+import android.os.Bundle;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
+import org.thoughtcrime.securesms.util.CommunicationActions;
+
+public class ResearchMegaphoneDialog extends FullScreenDialogFragment {
+
+ private static final String SURVEY_URL = "https://surveys.signalusers.org/s3";
+
+ @Override
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ TextView content = view.findViewById(R.id.research_megaphone_content);
+ content.setText(Html.fromHtml(requireContext().getString(R.string.ResearchMegaphoneDialog_we_believe_in_privacy)));
+
+ view.findViewById(R.id.research_megaphone_dialog_take_the_survey)
+ .setOnClickListener(v -> CommunicationActions.openBrowserLink(requireContext(), SURVEY_URL));
+
+ view.findViewById(R.id.research_megaphone_dialog_no_thanks)
+ .setOnClickListener(v -> dismissAllowingStateLoss());
+
+ return view;
+ }
+
+ @Override
+ protected @StringRes int getTitle() {
+ return R.string.ResearchMegaphoneDialog_signal_research;
+ }
+
+ @Override
+ protected int getDialogLayoutResource() {
+ return R.layout.research_megaphone_dialog;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/ShowForDurationSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ShowForDurationSchedule.java
new file mode 100644
index 000000000..baf5114c6
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ShowForDurationSchedule.java
@@ -0,0 +1,25 @@
+package org.thoughtcrime.securesms.megaphone;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Megaphone schedule that will always show for some duration after the first
+ * time the user sees it.
+ */
+public class ShowForDurationSchedule implements MegaphoneSchedule {
+
+ private final long duration;
+
+ public static MegaphoneSchedule showForDays(int days) {
+ return new ShowForDurationSchedule(TimeUnit.DAYS.toMillis(days));
+ }
+
+ public ShowForDurationSchedule(long duration) {
+ this.duration = duration;
+ }
+
+ @Override
+ public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
+ return firstVisible == 0 || currentTime < firstVisible + duration;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java
index 193a3f413..c9003c974 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java
@@ -91,7 +91,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
private void initializeList() {
RecyclerView list = findViewById(R.id.message_details_list);
- adapter = new MessageDetailsAdapter(glideRequests);
+ adapter = new MessageDetailsAdapter(this, glideRequests);
list.setAdapter(adapter);
list.setItemAnimator(null);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java
index 92fd33068..9984261f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java
@@ -5,6 +5,7 @@ import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@@ -20,13 +21,15 @@ final class MessageDetailsAdapter extends ListAdapter(), conversationMessage.getMessageRecord().getRecipient(), null, false);
+ conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false);
}
private void bindErrorState(MessageRecord messageRecord) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java
index f1e033482..ff167a078 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java
@@ -43,17 +43,10 @@ public class IncomingMessageProcessor {
*/
public Processor acquire() {
lock.lock();
-
- Thread current = Thread.currentThread();
- Log.d(TAG, "Lock acquired by thread " + current.getId() + " (" + current.getName() + ")");
-
return new Processor(context);
}
private void release() {
- Thread current = Thread.currentThread();
- Log.d(TAG, "Lock about to be released by thread " + current.getId() + " (" + current.getName() + ")");
-
lock.unlock();
}
@@ -92,7 +85,7 @@ public class IncomingMessageProcessor {
}
private @Nullable String processMessage(@NonNull SignalServiceEnvelope envelope) {
- Log.i(TAG, "Received message. Inserting in PushDatabase.");
+ Log.i(TAG, "Received message " + envelope.getTimestamp() + ". Inserting in PushDatabase.");
long id = pushDatabase.insert(envelope);
@@ -109,7 +102,7 @@ public class IncomingMessageProcessor {
}
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
- Log.i(TAG, String.format(Locale.ENGLISH, "Received receipt: (XXXXX, %d)", envelope.getTimestamp()));
+ Log.i(TAG, "Received server receipt for " + envelope.getTimestamp());
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalHighTrustPush(context, envelope.getSourceAddress()).getId(), envelope.getTimestamp()),
System.currentTimeMillis());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
index 5f8ab8703..9a2a39f4f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
@@ -39,7 +39,7 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
- public static final int CURRENT_VERSION = 18;
+ public static final int CURRENT_VERSION = 20;
private static final class Version {
static final int LEGACY = 1;
@@ -60,6 +60,8 @@ public class ApplicationMigrations {
static final int VERSIONED_PROFILE = 16;
static final int PIN_OPT_OUT = 17;
static final int TRIM_SETTINGS = 18;
+ static final int THUMBNAIL_CLEANUP = 19;
+ static final int GV2 = 20;
}
/**
@@ -247,6 +249,14 @@ public class ApplicationMigrations {
jobs.put(Version.TRIM_SETTINGS, new TrimByLengthSettingsMigrationJob());
}
+ if (lastSeenVersion < Version.THUMBNAIL_CLEANUP) {
+ jobs.put(Version.THUMBNAIL_CLEANUP, new DatabaseMigrationJob());
+ }
+
+ if (lastSeenVersion < Version.GV2) {
+ jobs.put(Version.GV2, new AttributesMigrationJob());
+ }
+
return jobs;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AttributesMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/AttributesMigrationJob.java
new file mode 100644
index 000000000..92ff92818
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AttributesMigrationJob.java
@@ -0,0 +1,59 @@
+package org.thoughtcrime.securesms.migrations;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobmanager.Data;
+import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
+import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
+import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
+import org.thoughtcrime.securesms.logging.Log;
+
+/**
+ * Schedules a re-upload of the users attributes followed by a download of their profile.
+ */
+public final class AttributesMigrationJob extends MigrationJob {
+
+ private static final String TAG = Log.tag(AttributesMigrationJob.class);
+
+ public static final String KEY = "AttributesMigrationJob";
+
+ AttributesMigrationJob() {
+ this(new Parameters.Builder().build());
+ }
+
+ private AttributesMigrationJob(@NonNull Parameters parameters) {
+ super(parameters);
+ }
+
+ @Override
+ public boolean isUiBlocking() {
+ return false;
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @Override
+ public void performMigration() {
+ Log.i(TAG, "Scheduling attributes upload and profile refresh job chain");
+ ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob())
+ .then(new RefreshOwnProfileJob())
+ .enqueue();
+ }
+
+ @Override
+ boolean shouldRetry(@NonNull Exception e) {
+ return false;
+ }
+
+ public static class Factory implements Job.Factory {
+ @Override
+ public @NonNull AttributesMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
+ return new AttributesMigrationJob(parameters);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java
index 70bc66934..29f83cfb2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java
@@ -39,19 +39,13 @@ public class AudioSlide extends Slide {
}
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
- super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, null, null, null, null, null));
+ super(context, new UriAttachment(uri, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, null, null, null, null, null));
}
public AudioSlide(Context context, Attachment attachment) {
super(context, attachment);
}
- @Override
- @Nullable
- public Uri getThumbnailUri() {
- return null;
- }
-
@Override
public boolean hasPlaceholder() {
return true;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java
index 9559962c9..523044b9f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java
@@ -29,7 +29,7 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
@Override
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
- if (MediaUtil.hasVideoThumbnail(uri)) {
+ if (MediaUtil.hasVideoThumbnail(context, uri)) {
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000);
if (thumbnail != null) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java
index b95d6973e..b142b28ba 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java
@@ -26,11 +26,6 @@ public class GifSlide extends ImageSlide {
this.borderless = borderless;
}
- @Override
- public @Nullable Uri getThumbnailUri() {
- return getUri();
- }
-
@Override
public boolean isBorderless() {
return borderless;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java
index a34fb7200..5313eefc6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java
@@ -54,11 +54,6 @@ public class ImageSlide extends Slide {
return 0;
}
- @Override
- public @Nullable Uri getThumbnailUri() {
- return getUri();
- }
-
@Override
public boolean hasImage() {
return true;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
index b7fc59f38..324e638a2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
@@ -48,7 +48,8 @@ public class IncomingMediaMessage {
long expiresIn,
boolean expirationUpdate,
boolean viewOnce,
- boolean unidentified)
+ boolean unidentified,
+ Optional> sharedContacts)
{
this.from = from;
this.groupId = groupId.orNull();
@@ -64,6 +65,8 @@ public class IncomingMediaMessage {
this.unidentified = unidentified;
this.attachments.addAll(attachments);
+ this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
+
}
public IncomingMediaMessage(@NonNull RecipientId from,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java
index 614ac7ba2..6583a44f5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java
@@ -48,8 +48,8 @@ public abstract class MediaConstraints {
public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) {
try {
- return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize(context) && isWithinBounds(context, attachment.getDataUri())) ||
- (MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize(context) && isWithinBounds(context, attachment.getDataUri())) ||
+ return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize(context) && isWithinBounds(context, attachment.getUri())) ||
+ (MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize(context) && isWithinBounds(context, attachment.getUri())) ||
(MediaUtil.isAudio(attachment) && attachment.getSize() <= getAudioMaxSize(context)) ||
(MediaUtil.isVideo(attachment) && attachment.getSize() <= getVideoMaxSize(context)) ||
(MediaUtil.isFile(attachment) && attachment.getSize() <= getDocumentMaxSize(context));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java
index c4aebd5eb..daf19ab7e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java
@@ -4,43 +4,40 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.UriMatcher;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
-import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
import org.thoughtcrime.securesms.providers.PartProvider;
-import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.io.InputStream;
public class PartAuthority {
- private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part";
- private static final String THUMB_URI_STRING = "content://org.thoughtcrime.securesms/thumb";
- private static final String STICKER_URI_STRING = "content://org.thoughtcrime.securesms/sticker";
+ private static final String AUTHORITY = BuildConfig.APPLICATION_ID;
+ private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part";
+ private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker";
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
- private static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING);
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
private static final int PART_ROW = 1;
- private static final int THUMB_ROW = 2;
- private static final int PERSISTENT_ROW = 3;
- private static final int BLOB_ROW = 4;
- private static final int STICKER_ROW = 5;
+ private static final int PERSISTENT_ROW = 2;
+ private static final int BLOB_ROW = 3;
+ private static final int STICKER_ROW = 4;
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
- uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
- uriMatcher.addURI("org.thoughtcrime.securesms", "sticker/#", STICKER_ROW);
+ uriMatcher.addURI(AUTHORITY, "part/*/#", PART_ROW);
+ uriMatcher.addURI(AUTHORITY, "sticker/#", STICKER_ROW);
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW);
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW);
uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW);
@@ -49,13 +46,6 @@ public class PartAuthority {
public static InputStream getAttachmentThumbnailStream(@NonNull Context context, @NonNull Uri uri)
throws IOException
{
- String contentType = getAttachmentContentType(context, uri);
- int match = uriMatcher.match(uri);
-
- if (match == PART_ROW && MediaUtil.isVideoType(contentType)) {
- return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
- }
-
return getAttachmentStream(context, uri);
}
@@ -66,7 +56,6 @@ public class PartAuthority {
try {
switch (match) {
case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0);
- case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
case STICKER_ROW: return DatabaseFactory.getStickerDatabase(context).getStickerStream(ContentUris.parseId(uri));
case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
@@ -81,7 +70,6 @@ public class PartAuthority {
int match = uriMatcher.match(uri);
switch (match) {
- case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
@@ -100,7 +88,6 @@ public class PartAuthority {
int match = uriMatcher.match(uri);
switch (match) {
- case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
@@ -119,7 +106,6 @@ public class PartAuthority {
int match = uriMatcher.match(uri);
switch (match) {
- case THUMB_ROW:
case PART_ROW:
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
@@ -145,8 +131,7 @@ public class PartAuthority {
}
public static Uri getAttachmentThumbnailUri(AttachmentId attachmentId) {
- Uri uri = Uri.withAppendedPath(THUMB_CONTENT_URI, String.valueOf(attachmentId.getUniqueId()));
- return ContentUris.withAppendedId(uri, attachmentId.getRowId());
+ return getAttachmentDataUri(attachmentId);
}
public static Uri getStickerUri(long id) {
@@ -157,11 +142,18 @@ public class PartAuthority {
int match = uriMatcher.match(uri);
switch (match) {
case PART_ROW:
- case THUMB_ROW:
case PERSISTENT_ROW:
case BLOB_ROW:
return true;
}
return false;
}
+
+ public static boolean isAttachmentUri(@NonNull Uri uri) {
+ return uriMatcher.match(uri) == PART_ROW;
+ }
+
+ public static @NonNull AttachmentId requireAttachmentId(@NonNull Uri uri) {
+ return new PartUriParser(uri).getPartId();
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java
index 340cef1b9..cd3fa889f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java
@@ -9,17 +9,20 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.List;
public class PartParser {
- private static final String TAG = Log.tag(PartParser.class);
+ private static final String TAG = Log.tag(PartParser.class);
+ private static final List DOCUMENT_TYPES = Arrays.asList("text/vcard", "text/x-vcard");
public static String getMessageText(PduBody body) {
String bodyText = null;
for (int i=0;i