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 fileName = getFileName(); if (fileName.isPresent()) { - return Optional.of(getFileType(fileName)); + String fileType = getFileType(fileName); + if (!fileType.isEmpty()) { + return Optional.of(fileType); + } } return Optional.fromNullable(MediaUtil.getExtension(context, getUri())); @@ -246,13 +243,12 @@ public abstract class Slide { this.hasImage() == that.hasImage() && this.hasVideo() == that.hasVideo() && this.getTransferState() == that.getTransferState() && - Util.equals(this.getUri(), that.getUri()) && - Util.equals(this.getThumbnailUri(), that.getThumbnailUri()); + Util.equals(this.getUri(), that.getUri()); } @Override public int hashCode() { return Util.hashCode(getContentType(), hasAudio(), hasImage(), - hasVideo(), getUri(), getThumbnailUri(), getTransferState()); + hasVideo(), getUri(), getTransferState()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java index f4920301a..66b5de1bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -37,11 +37,6 @@ public class StickerSlide extends Slide { return 0; } - @Override - public @Nullable Uri getThumbnailUri() { - return getUri(); - } - @Override public boolean hasSticker() { return true; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java index 77cc7aad4..5fd0a9b48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -37,7 +37,11 @@ public class VideoSlide extends Slide { } public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, null, false, false, false, transformProperties)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, false, transformProperties)); + } + + public VideoSlide(Context context, Uri uri, long dataSize, int width, int height, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, width, height, MediaUtil.hasVideoThumbnail(context, uri), null, caption, null, null, null, false, false, false, transformProperties)); } public VideoSlide(Context context, Attachment attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 664d8ca7c..1a57ceb04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -383,7 +383,7 @@ public class DefaultMessageNotifier implements MessageNotifier { notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()), notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient(), replyMethod), replyMethod, - !isSingleNotificationContactJoined); + !isSingleNotificationContactJoined && notificationState.canReply()); builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()), notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp()); @@ -413,7 +413,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Notification notification = builder.build(); NotificationManagerCompat.from(context).notify(notificationId, notification); - Log.i(TAG, "Posted notification. " + notification.toString()); + Log.i(TAG, "Posted notification."); } private static void sendMultipleThreadNotification(@NonNull Context context, @@ -533,6 +533,8 @@ public class DefaultMessageNotifier implements MessageNotifier { } if (isUnreadMessage) { + boolean canReply = false; + if (KeyCachingService.isLocked(context)) { body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { @@ -545,6 +547,9 @@ public class DefaultMessageNotifier implements MessageNotifier { } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { body = ThreadBodyUtil.getFormattedBodyFor(context, record); slideDeck = ((MmsMessageRecord) record).getSlideDeck(); + canReply = true; + } else { + canReply = true; } boolean includeMessage = true; @@ -555,11 +560,12 @@ public class DefaultMessageNotifier implements MessageNotifier { } if (threadRecipients == null || includeMessage) { - notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false, record.isJoined())); + notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false, record.isJoined(), canReply)); } } if (hasUnreadReactions) { + CharSequence originalBody = body; for (ReactionRecord reaction : record.getReactions()) { Recipient reactionSender = Recipient.resolved(reaction.getAuthor()); if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) { @@ -569,7 +575,7 @@ public class DefaultMessageNotifier implements MessageNotifier { if (KeyCachingService.isLocked(context)) { body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); } else { - String text = SpanUtil.italic(getReactionMessageBody(context, record)).toString(); + String text = SpanUtil.italic(getReactionMessageBody(context, record, originalBody)).toString(); String[] parts = text.split(EMOJI_REPLACEMENT_STRING); SpannableStringBuilder builder = new SpannableStringBuilder(); @@ -589,7 +595,7 @@ public class DefaultMessageNotifier implements MessageNotifier { } if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true, record.isJoined())); + notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true, record.isJoined(), false)); } } } @@ -599,9 +605,8 @@ public class DefaultMessageNotifier implements MessageNotifier { return notificationState; } - private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record) { - CharSequence body = record.getDisplayBody(context); - boolean bodyIsEmpty = TextUtils.isEmpty(body); + private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record, @NonNull CharSequence body) { + boolean bodyIsEmpty = TextUtils.isEmpty(body); if (MessageRecordUtil.hasSharedContact(record)) { Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java index 1c91a34ab..9a966ac7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java @@ -7,6 +7,7 @@ public final class NotificationIds { public static final int MESSAGE_SUMMARY = 1338; public static final int APPLICATION_MIGRATION = 4242; public static final int SMS_IMPORT_COMPLETE = 31337; + public static final int PRE_REGISTRATION_SMS = 5050; public static final int THREAD = 50000; private NotificationIds() { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java index e8e417c60..7cbb786e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -28,6 +28,7 @@ public class NotificationItem { @Nullable private final SlideDeck slideDeck; private final boolean jumpToMessage; private final boolean isJoin; + private final boolean canReply; public NotificationItem(long id, boolean mms, @@ -40,7 +41,8 @@ public class NotificationItem { long messageReceivedTimestamp, @Nullable SlideDeck slideDeck, boolean jumpToMessage, - boolean isJoin) + boolean isJoin, + boolean canReply) { this.id = id; this.mms = mms; @@ -54,6 +56,7 @@ public class NotificationItem { this.slideDeck = slideDeck; this.jumpToMessage = jumpToMessage; this.isJoin = isJoin; + this.canReply = canReply; } public @NonNull Recipient getRecipient() { @@ -112,4 +115,8 @@ public class NotificationItem { private static void makeIntentUniqueToPreventMerging(@NonNull Intent intent) { intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); } + + public boolean canReply() { + return canReply; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java index df20bfc63..8650ecfbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -112,14 +112,17 @@ public class NotificationState { } public PendingIntent getMarkAsReadIntent(Context context, int notificationId) { - long[] threadArray = new long[threads.size()]; - int index = 0; + long[] threadArray = new long[threads.size()]; + int index = 0; + StringBuilder threadString = new StringBuilder(); for (long thread : threads) { - Log.i(TAG, "Added thread: " + thread); + threadString.append(thread).append(" "); threadArray[index++] = thread; } + Log.i(TAG, "Added threads: " + threadString.toString()); + Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION); intent.setClass(context, MarkReadReceiver.class); intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); @@ -205,5 +208,8 @@ public class NotificationState { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } + public boolean canReply() { + return notifications.size() == 1 && notifications.get(0).canReply(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 765103ee5..82a27347f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -344,8 +344,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } private static Optional getThumbnailUri(@Nullable Slide slide) { - if (slide != null && !slide.isInProgress() && slide.getThumbnailUri() != null) { - return Optional.of(slide.getThumbnailUri()); + if (slide != null && !slide.isInProgress() && slide.getUri() != null) { + return Optional.of(slide.getUri()); } else { return Optional.absent(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 5d147200d..7dd5a0f7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -62,7 +62,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -555,10 +559,15 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment final PhoneNumberPrivacyValues.PhoneNumberSharingMode[] value = { phoneNumberPrivacyValues.getPhoneNumberSharingMode() }; + Map items = items(requireContext()); + List modes = new ArrayList<>(items.keySet()); + CharSequence[] modeStrings = items.values().toArray(new CharSequence[0]); + int selectedMode = modes.indexOf(value[0]); + new AlertDialog.Builder(requireActivity()) .setTitle(R.string.preferences_app_protection__see_my_phone_number) .setCancelable(true) - .setSingleChoiceItems(items(requireContext()), value[0].ordinal(), (dialog, which) -> value[0] = PhoneNumberPrivacyValues.PhoneNumberSharingMode.values()[which]) + .setSingleChoiceItems(modeStrings, selectedMode, (dialog, which) -> value[0] = modes.get(which)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberSharingMode = value[0]; phoneNumberPrivacyValues.setPhoneNumberSharingMode(phoneNumberSharingMode); @@ -572,13 +581,14 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment return true; } - private CharSequence[] items(Context context) { - return new CharSequence[]{ - titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_see_description)), - titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_my_contacts), context.getString(R.string.PhoneNumberPrivacy_my_contacts_see_description)), - context.getString(R.string.PhoneNumberPrivacy_nobody) }; - } + private Map items(Context context) { + Map map = new LinkedHashMap<>(); + map.put(PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE, titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_see_description))); + map.put(PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY, context.getString(R.string.PhoneNumberPrivacy_nobody)); + + return map; + } } private final class PhoneNumberPrivacyWhoCanFindClickListener implements Preference.OnPreferenceClickListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 044c04e77..8a69538e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -153,7 +153,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { .ifNecessary() .onAllGranted(() -> { Log.i(TAG, "Queing backup..."); - ApplicationDependencies.getJobManager().add(new LocalBackupJob()); + ApplicationDependencies.getJobManager().add(new LocalBackupJob(true)); }) .withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups)) .execute(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java index eb393dab7..193f17e6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java @@ -99,6 +99,10 @@ public final class ProfileName implements Parcelable { givenName = StringUtil.trimToFit(givenName.trim(), ProfileName.MAX_PART_LENGTH); familyName = StringUtil.trimToFit(familyName.trim(), ProfileName.MAX_PART_LENGTH); + if (givenName.isEmpty() && familyName.isEmpty()) { + return EMPTY; + } + return new ProfileName(givenName, familyName); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java index 809f24b8d..6bd6ccf15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java @@ -26,7 +26,6 @@ import androidx.navigation.Navigation; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.dd.CircularProgressButton; -import com.google.android.gms.common.util.IOUtils; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; @@ -43,6 +42,7 @@ import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.StringUtil; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.text.AfterTextChanged; import org.thoughtcrime.securesms.util.views.LearnMoreTextView; @@ -62,7 +62,6 @@ import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.SHOW_ public class EditProfileFragment extends LoggingFragment { private static final String TAG = Log.tag(EditProfileFragment.class); - private static final String AVATAR_STATE = "avatar"; private static final short REQUEST_CODE_SELECT_AVATAR = 31726; private static final int MAX_GROUP_NAME_LENGTH = 32; @@ -136,20 +135,6 @@ public class EditProfileFragment extends LoggingFragment { viewModel.refreshUsername(); } - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putByteArray(AVATAR_STATE, viewModel.getAvatarSnapshot()); - } - - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - - if (savedInstanceState != null && savedInstanceState.containsKey(AVATAR_STATE)) { - viewModel.setAvatar(savedInstanceState.getByteArray(AVATAR_STATE)); - } - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -167,7 +152,7 @@ public class EditProfileFragment extends LoggingFragment { Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA); InputStream stream = BlobProvider.getInstance().getStream(requireContext(), result.getUri()); - return IOUtils.readInputStreamFully(stream); + return Util.readFully(stream); } catch (IOException ioException) { Log.w(TAG, ioException); return null; @@ -200,7 +185,8 @@ public class EditProfileFragment extends LoggingFragment { EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository, hasSavedInstanceState, groupId); - viewModel = ViewModelProviders.of(this, factory).get(EditProfileViewModel.class); + viewModel = ViewModelProviders.of(requireActivity(), factory) + .get(EditProfileViewModel.class); } private void initializeResources(@NonNull View view, boolean isEditingGroup) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index 3d515005c..261037362 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -91,11 +91,6 @@ class EditProfileViewModel extends ViewModel { return hasAvatar(); } - @MainThread - public byte[] getAvatarSnapshot() { - return internalAvatar.getValue(); - } - public void setGivenName(String givenName) { this.givenName.setValue(givenName); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java index e00a0c372..bb1eea230 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java @@ -5,12 +5,14 @@ import android.content.Context; import android.content.UriMatcher; import android.media.MediaDataSource; import android.net.Uri; + import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; @@ -43,9 +45,9 @@ public class BlobProvider { private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs"; private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs"; - public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms/blob"); - public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String PATH = "blob/*/*/*/*/*"; + public static final String AUTHORITY = BuildConfig.APPLICATION_ID; + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/blob"); + public static final String PATH = "blob/*/*/*/*/*"; private static final int STORAGE_TYPE_PATH_SEGMENT = 1; private static final int MIMETYPE_PATH_SEGMENT = 2; diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java index 3c663daaa..1703dff55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/DeprecatedPersistentBlobProvider.java @@ -4,16 +4,17 @@ 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.logging.Log; - import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.FileProviderUtil; import java.io.File; @@ -29,11 +30,11 @@ import java.io.InputStream; @Deprecated public class DeprecatedPersistentBlobProvider { - private static final String TAG = DeprecatedPersistentBlobProvider.class.getSimpleName(); + private static final String TAG = Log.tag(DeprecatedPersistentBlobProvider.class); - private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new"; + public static final String AUTHORITY = BuildConfig.APPLICATION_ID; + private static final String URI_STRING = "content://" + AUTHORITY + "/capture-new"; public static final Uri CONTENT_URI = Uri.parse(URI_STRING); - public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String EXPECTED_PATH_OLD = "capture/*/*/#"; public static final String EXPECTED_PATH_NEW = "capture-new/*/*/*/*/#"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/MmsBodyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/MmsBodyProvider.java index 3bf0a22a4..cc98728ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/MmsBodyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/MmsBodyProvider.java @@ -24,8 +24,10 @@ import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; + import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.logging.Log; import java.io.File; @@ -35,7 +37,8 @@ import java.io.OutputStream; public class MmsBodyProvider extends ContentProvider { private static final String TAG = MmsBodyProvider.class.getSimpleName(); - private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.provider.securesms.mms/mms"; + private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".mms"; + private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/mms"; public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING); private static final int SINGLE_ROW = 1; @@ -43,7 +46,7 @@ public class MmsBodyProvider extends ContentProvider { static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - uriMatcher.addURI("org.thoughtcrime.provider.securesms.mms", "mms/#", SINGLE_ROW); + uriMatcher.addURI(CONTENT_AUTHORITY, "mms/#", SINGLE_ROW); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java index d608648ec..2c9aafd35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java @@ -26,12 +26,14 @@ import android.net.Uri; import android.os.MemoryFile; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; -import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.logging.Log; +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.PartUriParser; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.MemoryFileUtil; @@ -46,7 +48,8 @@ public class PartProvider extends ContentProvider { private static final String TAG = PartProvider.class.getSimpleName(); - private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.provider.securesms/part"; + private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".part"; + private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/part"; private static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING); private static final int SINGLE_ROW = 1; @@ -54,7 +57,7 @@ public class PartProvider extends ContentProvider { static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - uriMatcher.addURI("org.thoughtcrime.provider.securesms", "part/*/#", SINGLE_ROW); + uriMatcher.addURI(CONTENT_AUTHORITY, "part/*/#", SINGLE_ROW); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index 3ba9f7f6d..11fa1f1f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; -import com.google.android.gms.common.Feature; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -29,7 +28,6 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -68,7 +66,7 @@ public class RecipientUtil { throw new AssertionError(recipient.getId() + " - No UUID or phone number!"); } - if (FeatureFlags.cds() && !recipient.getUuid().isPresent()) { + if (!recipient.getUuid().isPresent()) { Log.i(TAG, recipient.getId() + " is missing a UUID..."); RegisteredState state = DirectoryHelper.refreshDirectoryFor(context, recipient, false); @@ -88,16 +86,7 @@ public class RecipientUtil { public static @NonNull List toSignalServiceAddressesFromResolved(@NonNull Context context, @NonNull List recipients) throws IOException { - if (FeatureFlags.cds()) { - List recipientsWithoutUuids = Stream.of(recipients) - .map(Recipient::resolve) - .filterNot(Recipient::hasUuid) - .toList(); - - if (recipientsWithoutUuids.size() > 0) { - DirectoryHelper.refreshDirectoryFor(context, recipientsWithoutUuids, false); - } - } + ensureUuidsAreAvailable(context, recipients); return Stream.of(recipients) .map(Recipient::resolve) @@ -105,19 +94,28 @@ public class RecipientUtil { .toList(); } + public static void ensureUuidsAreAvailable(@NonNull Context context, @NonNull Collection recipients) + throws IOException + { + List recipientsWithoutUuids = Stream.of(recipients) + .map(Recipient::resolve) + .filterNot(Recipient::hasUuid) + .toList(); + + if (recipientsWithoutUuids.size() > 0) { + DirectoryHelper.refreshDirectoryFor(context, recipientsWithoutUuids, false); + } + } + public static boolean isBlockable(@NonNull Recipient recipient) { Recipient resolved = recipient.resolve(); return resolved.isPushGroup() || resolved.hasServiceIdentifier(); } public static List getEligibleForSending(@NonNull List recipients) { - if (FeatureFlags.cds()) { - return Stream.of(recipients) - .filter(r -> r.getRegistered() != RegisteredState.NOT_REGISTERED) - .toList(); - } else { - return recipients; - } + return Stream.of(recipients) + .filter(r -> r.getRegistered() != RegisteredState.NOT_REGISTERED) + .toList(); } /** @@ -241,7 +239,7 @@ public class RecipientUtil { @WorkerThread public static void shareProfileIfFirstSecureMessage(@NonNull Context context, @NonNull Recipient recipient) { - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient.getId()); if (isPreMessageRequestThread(context, threadId)) { return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java index 0263f1408..a1edfa619 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java @@ -77,7 +77,7 @@ public final class RegistrationNavigationActivity extends AppCompatActivity { switch (status.getStatusCode()) { case CommonStatusCodes.SUCCESS: - Optional code = VerificationCodeParser.parse(context, (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE)); + Optional code = VerificationCodeParser.parse((String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE)); if (code.isPresent()) { Log.i(TAG, "Received verification code."); handleVerificationCodeReceived(code.get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index 12e5483ea..c6088ba43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -258,7 +258,6 @@ public final class CodeVerificationRequest { identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true); - TextSecurePreferences.setVerifying(context, false); TextSecurePreferences.setPushRegistered(context, true); TextSecurePreferences.setPushServerPassword(context, credentials.getPassword()); TextSecurePreferences.setSignedPreKeyRegistered(context, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java index 3be999e8c..1cae0bf82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java @@ -77,8 +77,6 @@ public final class RegistrationCodeRequest { } private static void markAsVerifying(Context context) { - TextSecurePreferences.setVerifying(context, true); - TextSecurePreferences.setPushRegistered(context, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index 1034bd54d..083011a7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -6,19 +6,14 @@ import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.annimon.stream.Stream; -import java.io.IOException; -import java.util.List; -import java.util.LinkedList; - import org.signal.ringrtc.CameraControl; - import org.thoughtcrime.securesms.logging.Log; - import org.webrtc.Camera1Enumerator; import org.webrtc.Camera2Capturer; import org.webrtc.Camera2Enumerator; @@ -28,6 +23,9 @@ import org.webrtc.CapturerObserver; import org.webrtc.EglBase; import org.webrtc.SurfaceTextureHelper; +import java.util.LinkedList; +import java.util.List; + import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK; import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT; import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE; @@ -48,9 +46,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @NonNull private CameraState.Direction activeDirection; private boolean enabled; - public Camera(@NonNull Context context, + public Camera(@NonNull Context context, @NonNull CameraEventListener cameraEventListener, - @NonNull EglBase eglBase) + @NonNull EglBase eglBase, + @NonNull CameraState.Direction desiredCameraDirection) { this.context = context; this.cameraEventListener = cameraEventListener; @@ -58,13 +57,16 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa CameraEnumerator enumerator = getCameraEnumerator(context); cameraCount = enumerator.getDeviceNames().length; - CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT); + CameraState.Direction firstChoice = desiredCameraDirection.isUsable() ? desiredCameraDirection : FRONT; + + CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, firstChoice); if (capturerCandidate != null) { - activeDirection = FRONT; + activeDirection = firstChoice; } else { - capturerCandidate = createVideoCapturer(enumerator, BACK); + CameraState.Direction secondChoice = firstChoice.switchDirection(); + capturerCandidate = createVideoCapturer(enumerator, secondChoice); if (capturerCandidate != null) { - activeDirection = BACK; + activeDirection = secondChoice; } else { activeDirection = NONE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java index 45f3720b6..1e96643a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java @@ -32,6 +32,21 @@ public class CameraState { } public enum Direction { - FRONT, BACK, NONE, PENDING + FRONT, BACK, NONE, PENDING; + + public boolean isUsable() { + return this == FRONT || this == BACK; + } + + public Direction switchDirection() { + switch (this) { + case FRONT: + return BACK; + case BACK: + return FRONT; + default: + return this; + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index c450efc04..ab8adcca6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -32,6 +32,7 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; @@ -61,14 +62,14 @@ public class KeyCachingService extends Service { public static final int SERVICE_RUNNING_ID = 4141; - public static final String KEY_PERMISSION = "org.thoughtcrime.securesms.ACCESS_SECRETS"; - public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT"; - public static final String CLEAR_KEY_EVENT = "org.thoughtcrime.securesms.service.action.CLEAR_KEY_EVENT"; - public static final String LOCK_TOGGLED_EVENT = "org.thoughtcrime.securesms.service.action.LOCK_ENABLED_EVENT"; - private static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT"; - public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY"; - public static final String DISABLE_ACTION = "org.thoughtcrime.securesms.service.action.DISABLE"; - public static final String LOCALE_CHANGE_EVENT = "org.thoughtcrime.securesms.service.action.LOCALE_CHANGE_EVENT"; + public static final String KEY_PERMISSION = BuildConfig.APPLICATION_ID + ".ACCESS_SECRETS"; + public static final String NEW_KEY_EVENT = BuildConfig.APPLICATION_ID + ".service.action.NEW_KEY_EVENT"; + public static final String CLEAR_KEY_EVENT = BuildConfig.APPLICATION_ID + ".service.action.CLEAR_KEY_EVENT"; + public static final String LOCK_TOGGLED_EVENT = BuildConfig.APPLICATION_ID + ".service.action.LOCK_ENABLED_EVENT"; + private static final String PASSPHRASE_EXPIRED_EVENT = BuildConfig.APPLICATION_ID + ".service.action.PASSPHRASE_EXPIRED_EVENT"; + public static final String CLEAR_KEY_ACTION = BuildConfig.APPLICATION_ID + ".service.action.CLEAR_KEY"; + public static final String DISABLE_ACTION = BuildConfig.APPLICATION_ID + ".service.action.DISABLE"; + public static final String LOCALE_CHANGE_EVENT = BuildConfig.APPLICATION_ID + ".service.action.LOCALE_CHANGE_EVENT"; private DynamicLanguage dynamicLanguage = new DynamicLanguage(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java index 6a0f30edd..e52f95b3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java @@ -24,7 +24,7 @@ public class LocalBackupListener extends PersistentAlarmManagerListener { @Override protected long onAlarm(Context context, long scheduledTime) { if (TextSecurePreferences.isBackupEnabled(context)) { - ApplicationDependencies.getJobManager().add(new LocalBackupJob()); + ApplicationDependencies.getJobManager().add(new LocalBackupJob(false)); } return setNextBackupTimeToIntervalFromNow(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/VerificationCodeParser.java b/app/src/main/java/org/thoughtcrime/securesms/service/VerificationCodeParser.java index be1693b3d..229614604 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/VerificationCodeParser.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/VerificationCodeParser.java @@ -16,9 +16,7 @@ */ package org.thoughtcrime.securesms.service; -import android.content.Context; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import java.util.regex.Matcher; @@ -28,14 +26,14 @@ public class VerificationCodeParser { private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(.*\\D|^)([0-9]{3,4})-([0-9]{3,4}).*", Pattern.DOTALL); - public static Optional parse(Context context, String messageBody) { + public static Optional parse(String messageBody) { if (messageBody == null) { return Optional.absent(); } Matcher challengeMatcher = CHALLENGE_PATTERN.matcher(messageBody); - if (!challengeMatcher.matches() || !TextSecurePreferences.isVerifying(context)) { + if (!challengeMatcher.matches()) { return Optional.absent(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index e0e69fd32..5533e86cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -26,11 +26,13 @@ import org.signal.ringrtc.IceCandidate; import org.signal.ringrtc.Remote; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.WebRtcCallActivity; -import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer; +import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil; @@ -48,7 +50,6 @@ import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TelephonyUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver; import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager; @@ -56,9 +57,14 @@ import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager; import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager; +import org.webrtc.CapturerObserver; import org.webrtc.EglBase; import org.webrtc.PeerConnection; -import org.whispersystems.libsignal.IdentityKey; +import org.webrtc.VideoFrame; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.DjbECPublicKey; +import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -74,8 +80,11 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -105,6 +114,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String EXTRA_REMOTE_PEER = "remote_peer"; public static final String EXTRA_REMOTE_PEER_KEY = "remote_peer_key"; public static final String EXTRA_REMOTE_DEVICE = "remote_device"; + public static final String EXTRA_REMOTE_IDENTITY_KEY = "remote_identity_key"; public static final String EXTRA_OFFER_OPAQUE = "offer_opaque"; public static final String EXTRA_OFFER_SDP = "offer_sdp"; public static final String EXTRA_OFFER_TYPE = "offer_type"; @@ -119,6 +129,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String EXTRA_BROADCAST = "broadcast"; public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video"; + public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN"; + public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL"; public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING"; public static final String ACTION_DENY_CALL = "DENY_CALL"; public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"; @@ -159,15 +171,14 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String ACTION_ENDED_INTERNAL_FAILURE = "ENDED_INTERNAL_FAILURE"; public static final String ACTION_ENDED_SIGNALING_FAILURE = "ENDED_SIGNALING_FAILURE"; public static final String ACTION_ENDED_CONNECTION_FAILURE = "ENDED_CONNECTION_FAILURE"; - public static final String ACTION_ENDED_RX_OFFER_EXPIRED = "ENDED_RX_OFFER_EXPIRED"; - public static final String ACTION_ENDED_RX_OFFER_WHILE_ACTIVE = "ENDED_RX_OFFER_WHILE_ACTIVE"; + public static final String ACTION_RECEIVED_OFFER_EXPIRED = "RECEIVED_OFFER_EXPIRED"; + public static final String ACTION_RECEIVED_OFFER_WHILE_ACTIVE = "RECEIVED_OFFER_WHILE_ACTIVE"; public static final String ACTION_CALL_CONCLUDED = "CALL_CONCLUDED"; public static final int BUSY_TONE_LENGTH = 2000; private CameraState localCameraState = CameraState.UNKNOWN; private boolean microphoneEnabled = true; - private boolean remoteVideoEnabled = false; private boolean bluetoothAvailable = false; private boolean enableVideoOnCreate = false; private boolean isRemoteVideoOffer = false; @@ -188,11 +199,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, @Nullable private CallManager callManager; @Nullable private RemotePeer activePeer; @Nullable private RemotePeer busyPeer; + @Nullable private RemotePeer preJoinPeer; @Nullable private SparseArray peerMap; - @Nullable private TextureViewRenderer localRenderer; - @Nullable private TextureViewRenderer remoteRenderer; - @Nullable private EglBase eglBase; - @Nullable private Camera camera; + private long callStartTimestamp; + + @Nullable private EglBase eglBase; + @Nullable private BroadcastVideoSink localSink; + @Nullable private Camera camera; + + private final Map remoteParticipantMap = new LinkedHashMap<>(); private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService networkExecutor = Executors.newSingleThreadExecutor(); @@ -222,6 +237,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, serviceExecutor.execute(() -> { if (intent.getAction().equals(ACTION_RECEIVE_OFFER)) handleReceivedOffer(intent); else if (intent.getAction().equals(ACTION_RECEIVE_BUSY)) handleReceivedBusy(intent); + else if (intent.getAction().equals(ACTION_PRE_JOIN_CALL)) handlePreJoinCall(intent); + else if (intent.getAction().equals(ACTION_CANCEL_PRE_JOIN_CALL)) handleCancelPreJoinCall(); else if (intent.getAction().equals(ACTION_OUTGOING_CALL) && isIdle()) handleOutgoingCall(intent); else if (intent.getAction().equals(ACTION_DENY_CALL)) handleDenyCall(intent); else if (intent.getAction().equals(ACTION_LOCAL_HANGUP)) handleLocalHangup(intent); @@ -260,8 +277,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, else if (intent.getAction().equals(ACTION_ENDED_INTERNAL_FAILURE)) handleEndedInternalFailure(intent); else if (intent.getAction().equals(ACTION_ENDED_SIGNALING_FAILURE)) handleEndedSignalingFailure(intent); else if (intent.getAction().equals(ACTION_ENDED_CONNECTION_FAILURE)) handleEndedConnectionFailure(intent); - else if (intent.getAction().equals(ACTION_ENDED_RX_OFFER_EXPIRED)) handleEndedReceivedOfferExpired(intent); - else if (intent.getAction().equals(ACTION_ENDED_RX_OFFER_WHILE_ACTIVE)) handleEndedReceivedOfferWhileActive(intent); + else if (intent.getAction().equals(ACTION_RECEIVED_OFFER_EXPIRED)) handleReceivedOfferExpired(intent); + else if (intent.getAction().equals(ACTION_RECEIVED_OFFER_WHILE_ACTIVE)) handleReceivedOfferWhileActive(intent); else if (intent.getAction().equals(ACTION_CALL_CONCLUDED)) handleCallConcluded(intent); }); @@ -323,7 +340,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) { localCameraState = newCameraState; if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + } else if (preJoinPeer != null) { + sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -388,6 +407,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private void handleReceivedOffer(Intent intent) { CallId callId = getCallId(intent); RemotePeer remotePeer = getRemotePeer(intent); + byte[] remoteIdentityKey = intent.getByteArrayExtra(EXTRA_REMOTE_IDENTITY_KEY); Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); byte[] opaque = intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE); String sdp = intent.getStringExtra(EXTRA_OFFER_SDP); @@ -402,7 +422,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "PSTN line is busy."); intent.putExtra(EXTRA_BROADCAST, true); handleSendBusy(intent); - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, serverReceivedTimestamp); return; } @@ -411,11 +431,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer, intent.putExtra(EXTRA_BROADCAST, true); intent.putExtra(EXTRA_HANGUP_TYPE, HangupMessage.Type.NEED_PERMISSION.getCode()); handleSendHangup(intent); - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, serverReceivedTimestamp); return; } peerMap.append(remotePeer.hashCode(), remotePeer); + callStartTimestamp = serverReceivedTimestamp; Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); isRemoteVideoOffer = offerType == OfferMessage.Type.VIDEO_CALL; @@ -426,12 +447,69 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp); try { - callManager.receivedOffer(callId, remotePeer, remoteDevice, opaque, sdp, messageAgeSec, callType, 1, isMultiRing, true); - } catch (CallException e) { + remoteIdentityKey = getPublicKeyBytes(remoteIdentityKey); + + byte[] localIdentityKey = getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(this).serialize()); + + callManager.receivedOffer(callId, remotePeer, remoteDevice, opaque, sdp, messageAgeSec, callType, 1, isMultiRing, true, remoteIdentityKey, localIdentityKey); + } catch (CallException | InvalidKeyException e) { callFailure("Unable to process received offer: ", e); } } + private void handlePreJoinCall(Intent intent) { + Log.i(TAG, "handlePreJoinCall():"); + + RemotePeer remotePeer = getRemotePeer(intent); + + if (remotePeer.getState() != CallState.IDLE) { + throw new IllegalStateException("Dialing from non-idle?"); + } + + preJoinPeer = remotePeer; + + initializeVideo(); + + localCameraState = initializeVanityCamera(); + + EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); + + sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, + remotePeer, + localCameraState, + bluetoothAvailable, + microphoneEnabled, + false); + } + + private @NonNull CameraState initializeVanityCamera() { + if (camera == null || localSink == null) { + return CameraState.UNKNOWN; + } + + if (camera.hasCapturer()) { + camera.initCapturer(new CapturerObserver() { + @Override + public void onFrameCaptured(VideoFrame videoFrame) { + localSink.onFrame(videoFrame); + } + + @Override + public void onCapturerStarted(boolean success) {} + + @Override + public void onCapturerStopped() {} + }); + camera.setEnabled(true); + } + return camera.getCameraState(); + } + + private void handleCancelPreJoinCall() { + cleanupVideo(); + preJoinPeer = null; + } + private void handleOutgoingCall(Intent intent) { Log.i(TAG, "handleOutgoingCall():"); @@ -441,13 +519,23 @@ public class WebRtcCallService extends Service implements CallManager.Observer, throw new IllegalStateException("Dialing from non-idle?"); } + preJoinPeer = null; + EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); peerMap.append(remotePeer.hashCode(), remotePeer); + callStartTimestamp = System.currentTimeMillis(); Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); initializeVideo(); + remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote( + remotePeer.getRecipient(), + null, + new BroadcastVideoSink(eglBase), + false + )); + OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); CallManager.CallMediaType callMediaType = getCallMediaTypeFromOfferType(offerType); @@ -466,8 +554,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - private void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal) { - Pair messageAndThreadId = DatabaseFactory.getSmsDatabase(this).insertMissedCall(remotePeer.getId()); + private void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp) { + Pair messageAndThreadId = DatabaseFactory.getSmsDatabase(this).insertMissedCall(remotePeer.getId(), timestamp); ApplicationDependencies.getMessageNotifier().updateNotification(this, messageAndThreadId.second(), signal); } @@ -486,7 +574,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, try { callManager.hangup(); - DatabaseFactory.getSmsDatabase(this).insertMissedCall(activePeer.getId()); + DatabaseFactory.getSmsDatabase(this).insertMissedCall(activePeer.getId(), System.currentTimeMillis()); terminate(activePeer); } catch (CallException e) { callFailure("hangup() failed: ", e); @@ -505,7 +593,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -519,7 +607,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -541,7 +629,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -552,7 +640,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer, camera.flip(); localCameraState = camera.getCameraState(); if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + } else if (preJoinPeer != null) { + sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } } @@ -561,7 +651,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false); if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -584,7 +674,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, audioManager.setSpeakerphoneOn(true); } - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } @@ -613,7 +703,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, AudioManager androidAudioManager = ServiceUtil.getAudioManager(this); androidAudioManager.setSpeakerphoneOn(false); - sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); lockManager.updatePhoneState(getInCallPhoneState()); audioManager.initializeAudioForCall(); audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING); @@ -633,8 +723,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, callManager.proceed(activePeer.getCallId(), WebRtcCallService.this, eglBase, - localRenderer, - remoteRenderer, + localSink, + remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(), camera, iceServers, isAlwaysTurn, @@ -645,7 +735,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, localCameraState = camera.getCameraState(); if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } }); @@ -668,6 +758,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, initializeVideo(); + remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote( + remotePeer.getRecipient(), + null, + new BroadcastVideoSink(eglBase), + false + )); + setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); retrieveTurnServers().addListener(new SuccessOnlyListener>(this.activePeer.getState(), this.activePeer.getCallId()) { @@ -680,8 +777,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, callManager.proceed(activePeer.getCallId(), WebRtcCallService.this, eglBase, - localRenderer, - remoteRenderer, + localSink, + remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(), camera, iceServers, hideIp, @@ -692,7 +789,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } }); @@ -753,10 +850,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleSendIceCandidates(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); + RemotePeer remotePeer = getRemotePeer(intent); + CallId callId = getCallId(intent); + Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); + boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); ArrayList iceCandidates = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); Log.i(TAG, "handleSendIceCandidates(): id: " + callId.format(remoteDevice)); @@ -806,24 +903,29 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleReceivedAnswer(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE); - String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP); - boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); + CallId callId = getCallId(intent); + byte[] remoteIdentityKey = intent.getByteArrayExtra(EXTRA_REMOTE_IDENTITY_KEY); + Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); + byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE); + String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP); + boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); Log.i(TAG, "handleReceivedAnswer(): id: " + callId.format(remoteDevice)); try { - callManager.receivedAnswer(callId, remoteDevice, opaque, sdp, isMultiRing); - } catch (CallException e) { + remoteIdentityKey = getPublicKeyBytes(remoteIdentityKey); + + byte[] localIdentityKey = getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(this).serialize()); + + callManager.receivedAnswer(callId, remoteDevice, opaque, sdp, isMultiRing, remoteIdentityKey, localIdentityKey); + } catch (CallException | InvalidKeyException e) { callFailure("receivedAnswer() failed: ", e); } } private void handleReceivedIceCandidates(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); + CallId callId = getCallId(intent); + Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); ArrayList iceCandidateParcels = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); Log.i(TAG, "handleReceivedIceCandidates(): id: " + callId.format(remoteDevice) + ", count: " + iceCandidateParcels.size()); @@ -857,8 +959,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleReceivedBusy(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); + CallId callId = getCallId(intent); + Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); Log.i(TAG, "handleReceivedBusy(): id: " + callId.format(remoteDevice)); @@ -883,7 +985,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, activePeer.localRinging(); lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); - sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(getApplicationContext(), recipient); if (shouldDisturbUserWithCall) { startCallCardActivityIfPossible(); @@ -914,7 +1016,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); activePeer.remoteRinging(); - sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } private void handleCallConnected(Intent intent) { @@ -940,7 +1042,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, callConnectedTime = System.currentTimeMillis(); - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); unregisterPowerButtonReceiver(); @@ -969,8 +1071,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId()); - remoteVideoEnabled = enable; - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + CallParticipant oldParticipant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient())); + CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable); + remoteParticipantMap.put(activePeer.getRecipient(), newParticipant); + + sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } @@ -979,7 +1084,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, AudioManager audioManager = ServiceUtil.getAudioManager(this); if (activePeer == null) { - Log.w(TAG, "handleSetEnableVideo(): Ignoring for inactive call."); + if (preJoinPeer != null) { + Log.w(TAG, "handleSetEnableVideo(): Changing for pre-join call."); + camera.setEnabled(enable); + enableVideoOnCreate = enable; + localCameraState = camera.getCameraState(); + sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + } else { + Log.w(TAG, "handleSetEnableVideo(): Ignoring for inactive call."); + } return; } @@ -1022,13 +1135,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, audioManager.setSpeakerphoneOn(true); } - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } private void handleLocalHangup(Intent intent) { if (activePeer == null) { if (busyPeer != null) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); busyPeer = null; } @@ -1043,32 +1156,32 @@ public class WebRtcCallService extends Service implements CallManager.Observer, try { callManager.hangup(); - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); terminate(activePeer); } catch (CallException e) { callFailure("hangup() failed: ", e); } } - private void handleEndedReceivedOfferExpired(Intent intent) { + private void handleReceivedOfferExpired(Intent intent) { RemotePeer remotePeer = getRemotePeerFromMap(intent); - Log.i(TAG, "handleEndedReceivedOfferExpired(): call_id: " + remotePeer.getCallId()); + Log.i(TAG, "handleReceivedOfferExpired(): call_id: " + remotePeer.getCallId()); - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, callStartTimestamp); terminate(remotePeer); } - private void handleEndedReceivedOfferWhileActive(Intent intent) { + private void handleReceivedOfferWhileActive(Intent intent) { RemotePeer remotePeer = getRemotePeerFromMap(intent); if (activePeer == null) { - Log.w(TAG, "handleEndedReceivedOfferWhileActive(): Ignoring for inactive call."); + Log.w(TAG, "handleReceivedOfferWhileActive(): Ignoring for inactive call."); return; } - Log.i(TAG, "handleEndedReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId()); + Log.i(TAG, "handleReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId()); switch (activePeer.getState()) { case DIALING: @@ -1084,7 +1197,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, stopForeground(true); } - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, callStartTimestamp); terminate(remotePeer); } @@ -1097,15 +1210,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, if (remotePeer.callIdEquals(activePeer)) { boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING; if (outgoingBeforeAccept) { - sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } else { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING; if (incomingBeforeAccept) { - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, callStartTimestamp); } terminate(remotePeer); @@ -1117,7 +1230,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedRemoteHangupAccepted(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } terminate(remotePeer); @@ -1129,7 +1242,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } terminate(remotePeer); @@ -1141,7 +1254,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } terminate(remotePeer); @@ -1163,7 +1276,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, busyPeer = null; }, BUSY_TONE_LENGTH); - sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } terminate(remotePeer); @@ -1175,7 +1288,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } terminate(remotePeer); @@ -1187,12 +1300,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING; if (incomingBeforeAccept) { - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, callStartTimestamp); } terminate(remotePeer); @@ -1204,11 +1317,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) { - insertMissedCall(remotePeer, true); + insertMissedCall(remotePeer, true, callStartTimestamp); } terminate(remotePeer); @@ -1252,21 +1365,46 @@ public class WebRtcCallService extends Service implements CallManager.Observer, return activePeer == null; } + private static byte[] getPublicKeyBytes(byte[] identityKey) throws InvalidKeyException { + ECPublicKey key = Curve.decodePoint(identityKey, 0); + + if (key instanceof DjbECPublicKey) { + return ((DjbECPublicKey) key).getPublicKey(); + } + throw new InvalidKeyException(); + } + private void initializeVideo() { Util.runOnMainSync(() -> { + if (eglBase == null) { + eglBase = EglBase.create(); + localSink = new BroadcastVideoSink(eglBase); + } - eglBase = EglBase.create(); - localRenderer = new TextureViewRenderer(WebRtcCallService.this); - remoteRenderer = new TextureViewRenderer(WebRtcCallService.this); + if (camera != null) { + camera.setEnabled(false); + camera.dispose(); + } - localRenderer.init(eglBase.getEglBaseContext(), null); - remoteRenderer.init(eglBase.getEglBaseContext(), null); - - camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase); + camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase, localCameraState.getActiveDirection()); localCameraState = camera.getCameraState(); }); } + private void cleanupVideo() { + if (camera != null) { + camera.dispose(); + camera = null; + } + + if (eglBase != null) { + eglBase.release(); + eglBase = null; + } + + localCameraState = CameraState.UNKNOWN; + } + private void setCallInProgressNotification(int type, RemotePeer remotePeer) { startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type), CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient())); @@ -1294,36 +1432,22 @@ public class WebRtcCallService extends Service implements CallManager.Observer, audioManager.stop(playDisconnectSound); bluetoothStateManager.setWantsConnection(false); - if (camera != null) { - camera.dispose(); - camera = null; - } + cleanupVideo(); - if (eglBase != null && localRenderer != null && remoteRenderer != null) { - localRenderer.release(); - remoteRenderer.release(); - eglBase.release(); - - localRenderer = null; - remoteRenderer = null; - eglBase = null; - } - - this.localCameraState = CameraState.UNKNOWN; this.microphoneEnabled = true; - this.remoteVideoEnabled = false; this.enableVideoOnCreate = false; Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); this.activePeer = null; + remoteParticipantMap.clear(); + lockManager.updatePhoneState(LockManager.PhoneState.IDLE); } private void sendMessage(@NonNull WebRtcViewModel.State state, @NonNull RemotePeer remotePeer, @NonNull CameraState localCameraState, - boolean remoteVideoEnabled, boolean bluetoothAvailable, boolean microphoneEnabled, boolean isRemoteVideoOffer) @@ -1331,35 +1455,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer, EventBus.getDefault().postSticky(new WebRtcViewModel(state, remotePeer.getRecipient(), localCameraState, - localRenderer, - remoteRenderer, - remoteVideoEnabled, + localSink, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer, - callConnectedTime)); - } - - private void sendMessage(@NonNull WebRtcViewModel.State state, - @NonNull RemotePeer remotePeer, - @NonNull IdentityKey identityKey, - @NonNull CameraState localCameraState, - boolean remoteVideoEnabled, - boolean bluetoothAvailable, - boolean microphoneEnabled, - boolean isRemoteVideoOffer) - { - EventBus.getDefault().postSticky(new WebRtcViewModel(state, - remotePeer.getRecipient(), - identityKey, - localCameraState, - localRenderer, - remoteRenderer, - remoteVideoEnabled, - bluetoothAvailable, - microphoneEnabled, - isRemoteVideoOffer, - callConnectedTime)); + callConnectedTime, + new ArrayList<>(remoteParticipantMap.values()))); } private ListenableFutureTask sendMessage(@NonNull final RemotePeer remotePeer, @@ -1429,7 +1530,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.w(TAG, "callFailure(): " + message, error); if (activePeer != null) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } if (callManager != null) { @@ -1601,7 +1702,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private abstract class StateAwareListener implements FutureTaskListener { - private final CallState expectedState; private final CallId expectedCallId; @@ -1710,11 +1810,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } if (error instanceof UntrustedIdentityException) { - sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, ((UntrustedIdentityException)error).getIdentityKey(), localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + CallParticipant participant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient())); + CallParticipant untrusted = participant.withIdentityKey(((UntrustedIdentityException) error).getIdentityKey()); + + remoteParticipantMap.put(activePeer.getRecipient(), untrusted); + + sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } else if (error instanceof UnregisteredUserException) { - sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } else if (error instanceof IOException) { - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } } } @@ -1837,15 +1942,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer, case ENDED_CONNECTION_FAILURE: intent.setAction(ACTION_ENDED_CONNECTION_FAILURE); break; - case ENDED_RECEIVED_OFFER_EXPIRED: - intent.setAction(ACTION_ENDED_RX_OFFER_EXPIRED); + case RECEIVED_OFFER_EXPIRED: + intent.setAction(ACTION_RECEIVED_OFFER_EXPIRED); break; - case ENDED_RECEIVED_OFFER_WHILE_ACTIVE: - intent.setAction(ACTION_ENDED_RX_OFFER_WHILE_ACTIVE); + case RECEIVED_OFFER_WHILE_ACTIVE: + case RECEIVED_OFFER_WITH_GLARE: + intent.setAction(ACTION_RECEIVED_OFFER_WHILE_ACTIVE); break; case ENDED_LOCAL_HANGUP: case ENDED_APP_DROPPED_CALL: - case ENDED_IGNORE_CALLS_FROM_NON_MULTIRING_CALLERS: + case IGNORE_CALLS_FROM_NON_MULTIRING_CALLERS: Log.i(TAG, "Ignoring event: " + event); return; default: @@ -1935,10 +2041,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } intent.setAction(ACTION_SEND_ICE_CANDIDATES) - .putExtra(EXTRA_CALL_ID, callId.longValue()) - .putExtra(EXTRA_REMOTE_PEER, remotePeer) - .putExtra(EXTRA_REMOTE_DEVICE, remoteDevice) - .putExtra(EXTRA_BROADCAST, broadcast) + .putExtra(EXTRA_CALL_ID, callId.longValue()) + .putExtra(EXTRA_REMOTE_PEER, remotePeer) + .putExtra(EXTRA_REMOTE_DEVICE, remoteDevice) + .putExtra(EXTRA_BROADCAST, broadcast) .putParcelableArrayListExtra(EXTRA_ICE_CANDIDATES, iceCandidateParcels); startService(intent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java index 67987a204..ea031e2fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java @@ -160,7 +160,7 @@ public class ShareActivity extends PassphraseRequiredActivity recipient = Recipient.external(this, number); } - long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); + long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()); return new Pair<>(existingThread, recipient); }, result -> onDestinationChosen(result.first(), result.second().getId())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java index 702a1c998..60f35ec65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; -import com.google.android.gms.common.util.Hex; +import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.StickerDatabase; @@ -77,8 +77,8 @@ public final class StickerPackPreviewRepository { @WorkerThread private Optional getManifestRemote(@NonNull String packId, @NonNull String packKey) { try { - byte[] packIdBytes = Hex.stringToBytes(packId); - byte[] packKeyBytes = Hex.stringToBytes(packKey); + byte[] packIdBytes = Hex.fromStringCondensed(packId); + byte[] packKeyBytes = Hex.fromStringCondensed(packKey); SignalServiceStickerManifest remoteManifest = receiver.retrieveStickerManifest(packIdBytes, packKeyBytes); StickerManifest localManifest = new StickerManifest(packId, packKey, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRemoteUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRemoteUriFetcher.java index 35c2244cc..6cad3ea6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRemoteUriFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRemoteUriFetcher.java @@ -5,7 +5,7 @@ import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; -import com.google.android.gms.common.util.Hex; +import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.logging.Log; import org.whispersystems.libsignal.InvalidMessageException; @@ -32,8 +32,8 @@ public final class StickerRemoteUriFetcher implements DataFetcher { @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { - byte[] packIdBytes = Hex.stringToBytes(stickerUri.getPackId()); - byte[] packKeyBytes = Hex.stringToBytes(stickerUri.getPackKey()); + byte[] packIdBytes = Hex.fromStringCondensed(stickerUri.getPackId()); + byte[] packKeyBytes = Hex.fromStringCondensed(stickerUri.getPackKey()); InputStream stream = receiver.retrieveSticker(packIdBytes, packKeyBytes, stickerUri.getStickerId()); callback.onDataReady(stream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerUrl.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerUrl.java index be8f25a65..494526359 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerUrl.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerUrl.java @@ -5,7 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; -import com.google.android.gms.common.util.Hex; +import org.thoughtcrime.securesms.util.Hex; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -70,7 +70,7 @@ public class StickerUrl { private static boolean isValidHex(String value) { try { - Hex.stringToBytes(value); + Hex.fromStringCondensed(value); return true; } catch (Exception e) { return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BucketingUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BucketingUtil.java new file mode 100644 index 000000000..661d502a8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BucketingUtil.java @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +/** + * Logic to bucket a user for a given feature flag based on their UUID. + */ +public final class BucketingUtil { + + private BucketingUtil() {} + + /** + * Calculate a user bucket for a given feature flag, uuid, and part per modulus. + * + * @param key Feature flag key (e.g., "research.megaphone.1") + * @param uuid Current user's UUID (see {@link Recipient#getUuid()}) + * @param modulus Drives the bucketing parts per N (e.g., passing 1,000,000 indicates bucketing into parts per million) + */ + public static long bucket(@NonNull String key, @NonNull UUID uuid, long modulus) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + + digest.update(key.getBytes()); + digest.update(".".getBytes()); + + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.order(ByteOrder.BIG_ENDIAN); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + + digest.update(byteBuffer.array()); + + return new BigInteger(Arrays.copyOfRange(digest.digest(), 0, 8)).mod(BigInteger.valueOf(modulus)).longValue(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 58ceab0be..511ae27df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -81,16 +81,7 @@ public class CommunicationActions { WebRtcCallService.isCallActive(activity, new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode == 1) { - startCallInternal(activity, recipient, false); - } else { - new AlertDialog.Builder(activity) - .setMessage(R.string.CommunicationActions_start_video_call) - .setPositiveButton(R.string.CommunicationActions_call, (d, w) -> startCallInternal(activity, recipient, true)) - .setNegativeButton(R.string.CommunicationActions_cancel, (d, w) -> d.dismiss()) - .setCancelable(true) - .show(); - } + startCallInternal(activity, recipient, resultCode != 1); } }); } @@ -268,13 +259,11 @@ public class CommunicationActions { .withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity))) .onAllGranted(() -> { Intent intent = new Intent(activity, WebRtcCallService.class); - intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL) + intent.setAction(WebRtcCallService.ACTION_PRE_JOIN_CALL) .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId())) .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode()); activity.startService(intent); - MessageSender.onMessageSent(); - Intent activityIntent = new Intent(activity, WebRtcCallActivity.class); activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 16081f41a..4949c623f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -12,8 +12,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; -import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; @@ -50,22 +48,17 @@ public final class FeatureFlags { private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2); private static final String USERNAMES = "android.usernames"; - private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; private static final String REMOTE_DELETE = "android.remoteDelete"; - private static final String GROUPS_V2_OLD_1 = "android.groupsv2"; - private static final String GROUPS_V2_OLD_2 = "android.groupsv2.2"; - private static final String GROUPS_V2_OLD_3 = "android.groupsv2.3"; - private static final String GROUPS_V2 = "android.groupsv2.4"; private static final String GROUPS_V2_CREATE_VERSION = "android.groupsv2.createVersion"; private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion"; private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion"; private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize"; - private static final String CDS_VERSION = "android.cdsVersion"; private static final String INTERNAL_USER = "android.internalUser"; private static final String MENTIONS = "android.mentions"; private static final String VERIFY_V2 = "android.verifyV2"; private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion"; private static final String CLIENT_EXPIRATION = "android.clientExpiration"; + public static final String RESEARCH_MEGAPHONE_1 = "research.megaphone.1"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -73,19 +66,17 @@ public final class FeatureFlags { */ private static final Set REMOTE_CAPABLE = Sets.newHashSet( - ATTACHMENTS_V3, REMOTE_DELETE, - GROUPS_V2, GROUPS_V2_CREATE_VERSION, GROUPS_V2_CAPACITY, GROUPS_V2_JOIN_VERSION, GROUPS_V2_LINKS_VERSION, - CDS_VERSION, INTERNAL_USER, USERNAMES, MENTIONS, VERIFY_V2, - CLIENT_EXPIRATION + CLIENT_EXPIRATION, + RESEARCH_MEGAPHONE_1 ); /** @@ -106,11 +97,9 @@ public final class FeatureFlags { * more burden on the reader to ensure that the app experience remains consistent. */ private static final Set HOT_SWAPPABLE = Sets.newHashSet( - ATTACHMENTS_V3, GROUPS_V2_CREATE_VERSION, GROUPS_V2_JOIN_VERSION, VERIFY_V2, - CDS_VERSION, CLIENT_EXPIRATION ); @@ -118,10 +107,6 @@ public final class FeatureFlags { * Flags in this set will stay true forever once they receive a true value from a remote config. */ private static final Set STICKY = Sets.newHashSet( - GROUPS_V2, - GROUPS_V2_OLD_1, - GROUPS_V2_OLD_2, - GROUPS_V2_OLD_3, VERIFY_V2 ); @@ -137,13 +122,6 @@ public final class FeatureFlags { * desired test state. */ private static final Map FLAG_CHANGE_LISTENERS = new HashMap() {{ - put(GROUPS_V2, (change) -> { - if (change == Change.ENABLED) { - ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob()) - .then(new RefreshOwnProfileJob()) - .enqueue(); - } - }); }}; private static final Map REMOTE_VALUES = new TreeMap<>(); @@ -196,42 +174,20 @@ public final class FeatureFlags { return getBoolean(USERNAMES, false); } - /** Whether or not we use the attachments v3 form. */ - public static boolean attachmentsV3() { - return getBoolean(ATTACHMENTS_V3, false); - } - /** Send support for remotely deleting a message. */ public static boolean remoteDelete() { return getBoolean(REMOTE_DELETE, false); } - /** Groups v2 send and receive. */ - public static boolean groupsV2() { - return groupsV2OlderStickyFlags() || groupsV2LatestFlag(); - } - /** Attempt groups v2 creation. */ public static boolean groupsV2create() { - return groupsV2LatestFlag() && - getVersionFlag(GROUPS_V2_CREATE_VERSION) == VersionFlag.ON && + return getVersionFlag(GROUPS_V2_CREATE_VERSION) == VersionFlag.ON && !SignalStore.internalValues().gv2DoNotCreateGv2Groups(); } /** Allow creation and managing of group links. */ public static boolean groupsV2manageGroupLinks() { - return groupsV2() && getVersionFlag(GROUPS_V2_LINKS_VERSION) == VersionFlag.ON; - } - - private static boolean groupsV2LatestFlag() { - return getBoolean(GROUPS_V2, false); - } - - /** Clients that previously saw these flags as true must continue to respect that */ - private static boolean groupsV2OlderStickyFlags() { - return getBoolean(GROUPS_V2_OLD_1, false) || - getBoolean(GROUPS_V2_OLD_2, false) || - getBoolean(GROUPS_V2_OLD_3, false); + return getVersionFlag(GROUPS_V2_LINKS_VERSION) == VersionFlag.ON; } /** @@ -271,14 +227,9 @@ public final class FeatureFlags { return getBoolean(INTERNAL_USER, false); } - /** Whether or not to use the new contact discovery service endpoint, which supports UUIDs. */ - public static boolean cds() { - return getVersionFlag(CDS_VERSION) == VersionFlag.ON; - } - /** Whether or not we allow mentions send support in groups. */ public static boolean mentions() { - return groupsV2() && getBoolean(MENTIONS, false); + return getBoolean(MENTIONS, false); } /** Whether or not to use the UUID in verification codes. */ @@ -291,6 +242,11 @@ public final class FeatureFlags { return getString(CLIENT_EXPIRATION, null); } + /** The raw research megaphone CSV string */ + public static String researchMegaphone() { + return getString(RESEARCH_MEGAPHONE_1, ""); + } + /** * Whether the user can choose phone number privacy settings, and; * Whether to fetch and store the secondary certificate diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java index 44de0078d..12359673c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java @@ -7,11 +7,13 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.core.content.FileProvider; +import org.thoughtcrime.securesms.BuildConfig; + import java.io.File; public class FileProviderUtil { - private static final String AUTHORITY = "org.thoughtcrime.securesms.fileprovider"; + private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; public static Uri getUriFor(@NonNull Context context, @NonNull File file) { if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FullscreenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/util/FullscreenHelper.java new file mode 100644 index 000000000..5b50d9f7c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FullscreenHelper.java @@ -0,0 +1,97 @@ +package org.thoughtcrime.securesms.util; + +import android.app.Activity; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; + +/** + * Encapsulates logic to properly show/hide system UI/chrome in a full screen setting. Also + * handles adjusting to notched devices as long as you call {@link #configureToolbarSpacer(View)}. + */ +public final class FullscreenHelper { + + @NonNull private final Activity activity; + + public FullscreenHelper(@NonNull Activity activity) { + this.activity = activity; + + if (Build.VERSION.SDK_INT >= 28) { + activity.getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + + showSystemUI(); + } + + public void configureToolbarSpacer(@NonNull View spacer) { + if (Build.VERSION.SDK_INT == 19) { + setSpacerHeight(spacer, ViewUtil.getStatusBarHeight(spacer)); + return; + } + + ViewCompat.setOnApplyWindowInsetsListener(spacer, (view, insets) -> { + setSpacerHeight(view, insets.getSystemWindowInsetTop()); + return insets; + }); + } + + private void setSpacerHeight(@NonNull View spacer, int height) { + ViewGroup.LayoutParams params = spacer.getLayoutParams(); + + params.height = height; + + spacer.setLayoutParams(params); + spacer.setVisibility(View.VISIBLE); + } + + public 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) + .withStartAction(() -> { + if (!hide) { + view.setVisibility(View.VISIBLE); + } + }) + .withEndAction(() -> { + if (hide) { + view.setVisibility(View.INVISIBLE); + } + }) + .start(); + } + }); + } + + public void toggleUiVisibility() { + int systemUiVisibility = activity.getWindow().getDecorView().getSystemUiVisibility(); + if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) { + showSystemUI(); + } else { + hideSystemUI(); + } + } + + public void hideSystemUI() { + activity.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); + } + + public void showSystemUI() { + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java index 07a250c29..daf5aa0e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java @@ -113,20 +113,20 @@ public class MappingAdapter extends ListAdapter, MappingViewHold } public interface Factory> { - @NonNull MappingViewHolder createViewHolder(ViewGroup parent); + @NonNull MappingViewHolder createViewHolder(@NonNull ViewGroup parent); } public static class LayoutFactory> implements Factory { private Function> creator; private final int layout; - public LayoutFactory(Function> creator, @LayoutRes int layout) { + public LayoutFactory(@NonNull Function> creator, @LayoutRes int layout) { this.creator = creator; this.layout = layout; } @Override - public @NonNull MappingViewHolder createViewHolder(ViewGroup parent) { + public @NonNull MappingViewHolder createViewHolder(@NonNull ViewGroup parent) { return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java index 13ae30abc..4b716a867 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java @@ -23,5 +23,9 @@ public abstract class MappingViewHolder> exten return itemView.findViewById(id); } + public @NonNull Context getContext() { + return itemView.getContext(); + } + public abstract void bind(@NonNull Model model); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java index 839e09767..f7847567b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java @@ -15,12 +15,15 @@ import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.gif.GifDrawable; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -152,7 +155,7 @@ public class MediaUtil { @WorkerThread public static Pair getDimensions(@NonNull Context context, @Nullable String contentType, @Nullable Uri uri) { - if (uri == null || !MediaUtil.isImageType(contentType)) { + if (uri == null || (!MediaUtil.isImageType(contentType) && !MediaUtil.isVideoType(contentType))) { return new Pair<>(0, 0); } @@ -161,18 +164,24 @@ public class MediaUtil { if (MediaUtil.isGif(contentType)) { try { GifDrawable drawable = GlideApp.with(context) - .asGif() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .load(new DecryptableUri(uri)) - .submit() - .get(); + .asGif() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .load(new DecryptableUri(uri)) + .submit() + .get(); dimens = new Pair<>(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } catch (InterruptedException e) { Log.w(TAG, "Was unable to complete work for GIF dimensions.", e); } catch (ExecutionException e) { Log.w(TAG, "Glide experienced an exception while trying to get GIF dimensions.", e); } + } else if (MediaUtil.hasVideoThumbnail(context, uri)) { + Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000); + + if (thumbnail != null) { + dimens = new Pair<>(thumbnail.getWidth(), thumbnail.getHeight()); + } } else { InputStream attachmentStream = null; try { @@ -297,12 +306,16 @@ public class MediaUtil { return (null != contentType) && contentType.equals(VIEW_ONCE); } - public static boolean hasVideoThumbnail(Uri uri) { + public static boolean hasVideoThumbnail(@NonNull Context context, @Nullable Uri uri) { + if (uri == null) { + return false; + } + if (BlobProvider.isAuthority(uri) && MediaUtil.isVideo(BlobProvider.getMimeType(uri)) && Build.VERSION.SDK_INT >= 23) { return true; } - if (uri == null || !isSupportedVideoUriScheme(uri.getScheme())) { + if (!isSupportedVideoUriScheme(uri.getScheme())) { return false; } @@ -313,14 +326,18 @@ public class MediaUtil { } else if (uri.toString().startsWith("file://") && MediaUtil.isVideo(URLConnection.guessContentTypeFromName(uri.toString()))) { return true; + } else if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) { + return true; } else { return false; } } @WorkerThread - public static @Nullable Bitmap getVideoThumbnail(Context context, Uri uri, long timeUs) { - if ("com.android.providers.media.documents".equals(uri.getAuthority())) { + public static @Nullable Bitmap getVideoThumbnail(@NonNull Context context, @Nullable Uri uri, long timeUs) { + if (uri == null) { + return null; + } else if ("com.android.providers.media.documents".equals(uri.getAuthority())) { long videoId = Long.parseLong(uri.getLastPathSegment().split(":")[1]); return MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), @@ -338,24 +355,44 @@ public class MediaUtil { MediaUtil.isVideo(URLConnection.guessContentTypeFromName(uri.toString()))) { return ThumbnailUtils.createVideoThumbnail(uri.toString().replace("file://", ""), MediaStore.Video.Thumbnails.MINI_KIND); - } else if (BlobProvider.isAuthority(uri) && - MediaUtil.isVideo(BlobProvider.getMimeType(uri)) && - Build.VERSION.SDK_INT >= 23) { + } else if (Build.VERSION.SDK_INT >= 23 && + BlobProvider.isAuthority(uri) && + MediaUtil.isVideo(BlobProvider.getMimeType(uri))) + { try { - MediaDataSource mediaDataSource = BlobProvider.getInstance().getMediaDataSource(context, uri); - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - - MediaMetadataRetrieverUtil.setDataSource(mediaMetadataRetriever, mediaDataSource); - return mediaMetadataRetriever.getFrameAtTime(timeUs); + MediaDataSource source = BlobProvider.getInstance().getMediaDataSource(context, uri); + return extractFrame(source, timeUs); } catch (IOException e) { - Log.w(TAG, "failed to get thumbnail for video blob uri: " + uri, e); - return null; + Log.w(TAG, "Failed to extract frame for URI: " + uri, e); + } + } else if (Build.VERSION.SDK_INT >= 23 && + PartAuthority.isAttachmentUri(uri) && + MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) + { + try { + AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri); + MediaDataSource source = DatabaseFactory.getAttachmentDatabase(context).mediaDataSourceFor(attachmentId); + return extractFrame(source, timeUs); + } catch (IOException e) { + Log.w(TAG, "Failed to extract frame for URI: " + uri, e); } } return null; } + @RequiresApi(23) + private static @Nullable Bitmap extractFrame(@Nullable MediaDataSource dataSource, long timeUs) throws IOException { + if (dataSource == null) { + return null; + } + + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + + MediaMetadataRetrieverUtil.setDataSource(mediaMetadataRetriever, dataSource); + return mediaMetadataRetriever.getFrameAtTime(timeUs); + } + public static @Nullable String getDiscreteMimeType(@NonNull String mimeType) { final String[] sections = mimeType.split("/", 2); return sections.length > 1 ? sections[0] : null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 1dfb2b3ea..3d01765a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -5,11 +5,13 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import androidx.navigation.ActionOnlyNavDirections; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; @@ -56,6 +58,8 @@ public final class ProfileUtil { } catch (ExecutionException e) { if (e.getCause() instanceof PushNetworkException) { throw (PushNetworkException) e.getCause(); + } else if (e.getCause() instanceof NotFoundException) { + throw (NotFoundException) e.getCause(); } else { throw new IOException(e); } @@ -68,7 +72,7 @@ public final class ProfileUtil { @NonNull Recipient recipient, @NonNull SignalServiceProfile.RequestType requestType) { - SignalServiceAddress address = RecipientUtil.toSignalServiceAddressBestEffort(context, recipient); + SignalServiceAddress address = toSignalServiceAddress(context, recipient); Optional unidentifiedAccess = getUnidentifiedAccess(context, recipient); Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); @@ -123,7 +127,7 @@ public final class ProfileUtil { } private static Optional getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) { - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); + Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient, false); if (unidentifiedAccess.isPresent()) { return unidentifiedAccess.get().getTargetUnidentifiedAccess(); @@ -131,4 +135,12 @@ public final class ProfileUtil { return Optional.absent(); } + + private static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) { + if (recipient.getRegistered() == RecipientDatabase.RegisteredState.NOT_REGISTERED) { + return new SignalServiceAddress(recipient.getUuid().orNull(), recipient.getE164().orNull()); + } else { + return RecipientUtil.toSignalServiceAddressBestEffort(context, recipient); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ResearchMegaphone.java b/app/src/main/java/org/thoughtcrime/securesms/util/ResearchMegaphone.java new file mode 100644 index 000000000..ac071918c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ResearchMegaphone.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parses a comma-separated list of country codes colon-separated from how many buckets out of 1 million + * should be enabled to see this megaphone in that country code. At the end of the list, an optional + * element saying how many buckets out of a million should be enabled for all countries not listed previously + * in the list. For example, "1:20000,*:40000" would mean 2% of the NANPA phone numbers and 4% of the rest of + * the world should see the megaphone. + */ +public final class ResearchMegaphone { + + private static final String TAG = Log.tag(ResearchMegaphone.class); + + private static final String COUNTRY_WILDCARD = "*"; + + /** + * In research megaphone group for given country code + */ + public static boolean isInResearchMegaphone() { + Map countryCountEnabled = parseCountryCounts(FeatureFlags.researchMegaphone()); + Recipient self = Recipient.self(); + + if (countryCountEnabled.isEmpty() || !self.getE164().isPresent() || !self.getUuid().isPresent()) { + return false; + } + + long countEnabled = determineCountEnabled(countryCountEnabled, self.getE164().or("")); + long currentUserBucket = BucketingUtil.bucket(FeatureFlags.RESEARCH_MEGAPHONE_1, self.requireUuid(), 1_000_000); + + return countEnabled > currentUserBucket; + } + + @VisibleForTesting + static @NonNull Map parseCountryCounts(@NonNull String buckets) { + Map countryCountEnabled = new HashMap<>(); + + for (String bucket : buckets.split(",")) { + String[] parts = bucket.split(":"); + if (parts.length == 2 && !parts[0].isEmpty()) { + countryCountEnabled.put(parts[0], Util.parseInt(parts[1], 0)); + } + } + return countryCountEnabled; + } + + @VisibleForTesting + static long determineCountEnabled(@NonNull Map countryCountEnabled, @NonNull String e164) { + Integer countEnabled = countryCountEnabled.get(COUNTRY_WILDCARD); + try { + String countryCode = String.valueOf(PhoneNumberUtil.getInstance().parse(e164, "").getCountryCode()); + if (countryCountEnabled.containsKey(countryCode)) { + countEnabled = countryCountEnabled.get(countryCode); + } + } catch (NumberParseException e) { + Log.d(TAG, "Unable to determine country code for bucketing."); + return 0; + } + + return countEnabled != null ? countEnabled : 0; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java index a7ed94afa..aed3c4482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java @@ -5,14 +5,12 @@ import android.app.ActivityManager; import android.app.AlarmManager; import android.app.NotificationManager; import android.app.job.JobScheduler; -import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.hardware.display.DisplayManager; import android.location.LocationManager; import android.media.AudioManager; import android.net.ConnectivityManager; -import android.os.Build; import android.os.PowerManager; import android.os.Vibrator; import androidx.annotation.NonNull; @@ -80,7 +78,7 @@ public class ServiceUtil { return (JobScheduler) context.getSystemService(JobScheduler.class); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) + @RequiresApi(22) public static @Nullable SubscriptionManager getSubscriptionManager(@NonNull Context context) { return (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SnapToTopDataObserver.java b/app/src/main/java/org/thoughtcrime/securesms/util/SnapToTopDataObserver.java index 7b8039b95..b38dc9a05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SnapToTopDataObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SnapToTopDataObserver.java @@ -24,14 +24,22 @@ public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver { private final LinearLayoutManager layoutManager; private final Deferred deferred; private final ScrollRequestValidator scrollRequestValidator; + private final ScrollToTop scrollToTop; + + public SnapToTopDataObserver(@NonNull RecyclerView recyclerView) { + this(recyclerView, null, null); + } public SnapToTopDataObserver(@NonNull RecyclerView recyclerView, - @Nullable ScrollRequestValidator scrollRequestValidator) + @Nullable ScrollRequestValidator scrollRequestValidator, + @Nullable ScrollToTop scrollToTop) { this.recyclerView = recyclerView; this.layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); this.deferred = new Deferred(); this.scrollRequestValidator = scrollRequestValidator; + this.scrollToTop = scrollToTop == null ? () -> layoutManager.scrollToPosition(0) + : scrollToTop; } /** @@ -108,7 +116,7 @@ public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver { } if (layoutManager.findFirstVisibleItemPosition() == 0) { - layoutManager.scrollToPosition(0); + scrollToTop.scrollToTop(); } } @@ -144,6 +152,13 @@ public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver { void onPerformScroll(@NonNull LinearLayoutManager layoutManager, int position); } + /** + * Method Object for scrolling to the top of a view, in case special handling is desired. + */ + public interface ScrollToTop { + void scrollToTop(); + } + public final class ScrollRequestBuilder { private final int position; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java index 52e7ab93c..77c7c2859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java @@ -2,14 +2,18 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.os.Environment; + import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.database.NoExternalStorageException; import java.io.File; public class StorageUtil { + private static final String PRODUCTION_PACKAGE_ID = "org.thoughtcrime.securesms"; + public static File getBackupDirectory() throws NoExternalStorageException { File storage = Environment.getExternalStorageDirectory(); @@ -20,6 +24,11 @@ public class StorageUtil { File signal = new File(storage, "Signal"); File backups = new File(signal, "Backups"); + //noinspection ConstantConditions + if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) { + backups = new File(backups, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1)); + } + if (!backups.exists()) { if (!backups.mkdirs()) { throw new NoExternalStorageException("Unable to create backup directory..."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 43d672b85..8c388ec07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -75,7 +75,6 @@ public class TextSecurePreferences { private static final String LOCAL_NUMBER_PREF = "pref_local_number"; private static final String LOCAL_UUID_PREF = "pref_local_uuid"; private static final String LOCAL_USERNAME_PREF = "pref_local_username"; - private static final String VERIFYING_STATE_PREF = "pref_verifying"; public static final String REGISTERED_GCM_PREF = "pref_gcm_registered"; private static final String GCM_PASSWORD_PREF = "pref_gcm_password"; private static final String SEEN_WELCOME_SCREEN_PREF = "pref_seen_welcome_screen"; @@ -840,14 +839,6 @@ public class TextSecurePreferences { return getStringPreference(context, THEME_PREF, DynamicTheme.systemThemeAvailable() ? DynamicTheme.SYSTEM : DynamicTheme.LIGHT); } - public static boolean isVerifying(Context context) { - return getBooleanPreference(context, VERIFYING_STATE_PREF, false); - } - - public static void setVerifying(Context context, boolean verifying) { - setBooleanPreference(context, VERIFYING_STATE_PREF, verifying); - } - public static boolean isPushRegistered(Context context) { return getBooleanPreference(context, REGISTERED_GCM_PREF, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index cec591636..86b0b444d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -664,6 +664,14 @@ public class Util { } } + public static int parseInt(String integer, int defaultValue) { + try { + return Integer.parseInt(integer); + } catch (NumberFormatException e) { + return defaultValue; + } + } + /** * Appends the stack trace of the provided throwable onto the provided primary exception. This is * useful for when exceptions are thrown inside of asynchronous systems (like runnables in an diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java index dfe57597e..49993a736 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java @@ -39,7 +39,6 @@ public class WakeLockUtil { WakeLock wakeLock = powerManager.newWakeLock(lockType, tag); wakeLock.acquire(timeout); - Log.d(TAG, "Acquired wakelock with tag: " + tag); return wakeLock; } catch (Exception e) { @@ -58,7 +57,6 @@ public class WakeLockUtil { Log.d(TAG, "Wakelock was null. Skipping. Tag: " + tag); } else if (wakeLock.isHeld()) { wakeLock.release(); - Log.d(TAG, "Released wakelock with tag: " + tag); } else { Log.d(TAG, "Wakelock wasn't held at time of release: " + tag); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java b/app/src/main/java/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java index 93fe665a9..a72741f7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java @@ -4,8 +4,6 @@ import android.os.HandlerThread; import androidx.annotation.NonNull; -import com.google.android.gms.common.util.concurrent.NumberedThreadFactory; - import org.thoughtcrime.securesms.util.LinkedBlockingLifoQueue; import java.util.Queue; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/DynamicLanguageContextWrapper.java b/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/DynamicLanguageContextWrapper.java index dc065a7c5..d4e227fb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/DynamicLanguageContextWrapper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/DynamicLanguageContextWrapper.java @@ -20,7 +20,9 @@ public final class DynamicLanguageContextWrapper { final Configuration config = resources.getConfiguration(); final Configuration newConfig = copyWithNewLocale(config, newLocale); - return context.createConfigurationContext(newConfig); + resources.updateConfiguration(newConfig, resources.getDisplayMetrics()); + + return context; } private static Configuration copyWithNewLocale(Configuration config, Locale locale) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java index 8e949ea2f..6f78577d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.util.livedata; +import android.os.Handler; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; @@ -11,7 +13,6 @@ import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Function; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -86,7 +87,7 @@ public final class LiveDataUtil { * Merges the supplied live data streams. */ public static LiveData merge(@NonNull List> liveDataList) { - Set> set = new LinkedHashSet<>(liveDataList); + Set> set = new LinkedHashSet<>(liveDataList.size()); set.addAll(liveDataList); @@ -110,6 +111,40 @@ public final class LiveDataUtil { return new MutableLiveData<>(item); } + /** + * Emits {@param whileWaiting} until {@param main} starts emitting. + */ + public static @NonNull LiveData until(@NonNull LiveData main, + @NonNull LiveData whileWaiting) + { + MediatorLiveData mediatorLiveData = new MediatorLiveData<>(); + + mediatorLiveData.addSource(whileWaiting, mediatorLiveData::setValue); + + mediatorLiveData.addSource(main, value -> { + mediatorLiveData.removeSource(whileWaiting); + mediatorLiveData.setValue(value); + }); + + return mediatorLiveData; + } + + /** + * After {@param delay} ms after observation, emits a single Object, {@param value}. + */ + public static LiveData delay(long delay, T value) { + return new MutableLiveData() { + boolean emittedValue; + + @Override + protected void onActive() { + if (emittedValue) return; + new Handler().postDelayed(() -> setValue(value), delay); + emittedValue = true; + } + }; + } + public interface Combine { @NonNull R apply(@NonNull A a, @NonNull B b); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java b/app/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java index 5a505bcbf..4d16a7f0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java @@ -3,32 +3,42 @@ package org.thoughtcrime.securesms.util.task; import android.app.ProgressDialog; import android.graphics.Color; import android.os.AsyncTask; -import androidx.annotation.Nullable; -import com.google.android.material.snackbar.Snackbar; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; + +import com.google.android.material.snackbar.Snackbar; + +import org.thoughtcrime.securesms.logging.Log; + public abstract class SnackbarAsyncTask extends AsyncTask implements View.OnClickListener { + private static final String TAG = Log.tag(SnackbarAsyncTask.class); - private final View view; - private final String snackbarText; - private final String snackbarActionText; - private final int snackbarActionColor; - private final int snackbarDuration; - private final boolean showProgress; + private final Lifecycle lifecycle; + private final View view; + private final String snackbarText; + private final String snackbarActionText; + private final int snackbarActionColor; + private final int snackbarDuration; + private final boolean showProgress; private @Nullable Params reversibleParameter; private @Nullable ProgressDialog progressDialog; - public SnackbarAsyncTask(View view, + public SnackbarAsyncTask(@NonNull Lifecycle lifecycle, + @NonNull View view, String snackbarText, String snackbarActionText, int snackbarActionColor, int snackbarDuration, boolean showProgress) { + this.lifecycle = lifecycle; this.view = view; this.snackbarText = snackbarText; this.snackbarActionText = snackbarActionText; @@ -58,6 +68,11 @@ public abstract class SnackbarAsyncTask this.progressDialog = null; } + if (!lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) { + Log.w(TAG, "Not in at least created state. Refusing to show snack bar."); + return; + } + Snackbar.make(view, snackbarText, snackbarDuration) .setAction(snackbarActionText, this) .setActionTextColor(snackbarActionColor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientMappingModel.java b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientMappingModel.java new file mode 100644 index 000000000..e07ee76c5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientMappingModel.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.util.viewholders; + +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 java.util.Objects; + +public abstract class RecipientMappingModel> implements MappingModel { + + public abstract @NonNull Recipient getRecipient(); + + public @NonNull String getName(@NonNull Context context) { + return getRecipient().getDisplayName(context); + } + + @Override + public boolean areItemsTheSame(@NonNull T newItem) { + return getRecipient().getId().equals(newItem.getRecipient().getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull T newItem) { + Context context = ApplicationDependencies.getApplication(); + return getName(context).equals(newItem.getName(context)) && Objects.equals(getRecipient().getContactPhoto(), newItem.getRecipient().getContactPhoto()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java new file mode 100644 index 000000000..de667bada --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.util.viewholders; + +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +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 RecipientViewHolder> extends MappingViewHolder { + + protected final @Nullable AvatarImageView avatar; + protected final @Nullable TextView name; + protected final @Nullable EventListener eventListener; + + public RecipientViewHolder(@NonNull View itemView, @Nullable EventListener eventListener) { + super(itemView); + this.eventListener = eventListener; + + avatar = findViewById(R.id.recipient_view_avatar); + name = findViewById(R.id.recipient_view_name); + } + + @Override + public void bind(@NonNull T model) { + if (avatar != null) { + avatar.setRecipient(model.getRecipient()); + } + + if (name != null) { + name.setText(model.getName(context)); + } + + if (eventListener != null) { + itemView.setOnClickListener(v -> eventListener.onModelClick(model)); + } else { + itemView.setOnClickListener(null); + } + } + + public static @NonNull > MappingAdapter.Factory createFactory(@LayoutRes int layout, @Nullable EventListener listener) { + return new MappingAdapter.LayoutFactory<>(view -> new RecipientViewHolder<>(view, listener), layout); + } + + public interface EventListener> { + default void onModelClick(@NonNull T model) { + onClick(model.getRecipient()); + } + + void onClick(@NonNull Recipient recipient); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/LearnMoreTextView.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/LearnMoreTextView.java index 6ead2bc2c..2715b5828 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/views/LearnMoreTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/LearnMoreTextView.java @@ -14,6 +14,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.widget.AppCompatTextView; import org.thoughtcrime.securesms.R; @@ -38,26 +39,7 @@ public class LearnMoreTextView extends AppCompatTextView { private void init() { setMovementMethod(LinkMovementMethod.getInstance()); - - ClickableSpan clickable = new ClickableSpan() { - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.colorAccent)); - } - - @Override - public void onClick(@NonNull View widget) { - if (linkListener != null) { - linkListener.onClick(widget); - } - } - }; - - link = new SpannableString(getContext().getString(R.string.LearnMoreTextView_learn_more)); - link.setSpan(clickable, 0, link.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - + setLinkTextInternal(R.string.LearnMoreTextView_learn_more); visible = true; } @@ -81,6 +63,33 @@ public class LearnMoreTextView extends AppCompatTextView { setTextInternal(baseText, visible ? BufferType.SPANNABLE : BufferType.NORMAL); } + public void setLearnMoreVisible(boolean visible, @StringRes int linkText) { + setLinkTextInternal(linkText); + this.visible = visible; + setTextInternal(baseText, visible ? BufferType.SPANNABLE : BufferType.NORMAL); + } + + private void setLinkTextInternal(@StringRes int linkText) { + ClickableSpan clickable = new ClickableSpan() { + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.colorAccent)); + } + + @Override + public void onClick(@NonNull View widget) { + if (linkListener != null) { + linkListener.onClick(widget); + } + } + }; + + link = new SpannableString(getContext().getString(linkText)); + link.setSpan(clickable, 0, link.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + private void setTextInternal(CharSequence text, BufferType type) { if (visible) { SpannableStringBuilder builder = new SpannableStringBuilder(); diff --git a/app/src/main/res/drawable-hdpi/signal_research.webp b/app/src/main/res/drawable-hdpi/signal_research.webp new file mode 100644 index 000000000..6a0648ac6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/signal_research.webp differ diff --git a/app/src/main/res/drawable-mdpi/signal_research.webp b/app/src/main/res/drawable-mdpi/signal_research.webp new file mode 100644 index 000000000..f0b6a30db Binary files /dev/null and b/app/src/main/res/drawable-mdpi/signal_research.webp differ diff --git a/app/src/main/res/drawable-xhdpi/signal_research.webp b/app/src/main/res/drawable-xhdpi/signal_research.webp new file mode 100644 index 000000000..797342d23 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/signal_research.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/signal_research.webp b/app/src/main/res/drawable-xxhdpi/signal_research.webp new file mode 100644 index 000000000..319a3d14c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/signal_research.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/signal_research.webp b/app/src/main/res/drawable-xxxhdpi/signal_research.webp new file mode 100644 index 000000000..6dadb13ff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/signal_research.webp differ diff --git a/app/src/main/res/drawable/ic_research_megaphone.xml b/app/src/main/res/drawable/ic_research_megaphone.xml new file mode 100644 index 000000000..308238f57 --- /dev/null +++ b/app/src/main/res/drawable/ic_research_megaphone.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/basic_megaphone_view.xml b/app/src/main/res/layout/basic_megaphone_view.xml index d9d535c47..2d5b86f60 100644 --- a/app/src/main/res/layout/basic_megaphone_view.xml +++ b/app/src/main/res/layout/basic_megaphone_view.xml @@ -23,7 +23,7 @@ android:scaleType="centerInside" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/profile_splash"/> + tools:src="@tools:sample/avatars"/>