mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 11:20:47 +01:00
Add support for Android 11 Conversation Bubbles.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
/**
|
||||
* Activity which encapsulates a conversation for a Bubble window.
|
||||
*
|
||||
* This activity is empty, and exists so that we can override some of its manifest parameters
|
||||
* without clashing with ConversationActivity.
|
||||
*/
|
||||
public class BubbleConversationActivity extends ConversationActivity {
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
@@ -241,6 +242,7 @@ import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
@@ -530,15 +532,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
|
||||
setVisibleThread(threadId);
|
||||
ConversationUtil.pushShortcutForRecipient(getApplicationContext(), recipientSnapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
if (!isInBubble()) {
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
}
|
||||
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_end);
|
||||
inputPanel.onPause();
|
||||
|
||||
@@ -717,6 +721,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
|
||||
}
|
||||
|
||||
private void setVisibleThread(long threadId) {
|
||||
if (!isInBubble()) {
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportShortcutLaunch(@NonNull RecipientId recipientId) {
|
||||
if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
return;
|
||||
@@ -870,6 +880,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings);
|
||||
}
|
||||
|
||||
hideMenuItem(menu, R.id.menu_create_bubble);
|
||||
viewModel.canShowAsBubble().observe(this, canShowAsBubble -> {
|
||||
MenuItem item = menu.findItem(R.id.menu_create_bubble);
|
||||
|
||||
if (item != null) {
|
||||
item.setVisible(canShowAsBubble && !isInBubble());
|
||||
}
|
||||
});
|
||||
|
||||
searchViewItem = menu.findItem(R.id.menu_search);
|
||||
|
||||
SearchView searchView = (SearchView) searchViewItem.getActionView();
|
||||
@@ -948,6 +967,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
|
||||
case R.id.menu_expiring_messages_off:
|
||||
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
|
||||
case R.id.menu_create_bubble: handleCreateBubble(); return true;
|
||||
case android.R.id.home: onNavigateUp(); return true;
|
||||
}
|
||||
|
||||
@@ -1220,6 +1240,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
}
|
||||
|
||||
private void handleCreateBubble() {
|
||||
ConversationIntents.Args args = viewModel.getArgs();
|
||||
|
||||
BubbleUtil.displayAsBubble(this, args.getRecipientId(), args.getThreadId());
|
||||
finish();
|
||||
}
|
||||
|
||||
private static void addIconToHomeScreen(@NonNull Context context,
|
||||
@NonNull Bitmap bitmap,
|
||||
@NonNull Recipient recipient)
|
||||
@@ -1933,6 +1960,21 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
|
||||
if (isInBubble()) {
|
||||
supportActionBar.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_notification));
|
||||
toolbar.setNavigationOnClickListener(unused -> startActivity(new Intent(Intent.ACTION_MAIN).setClass(this, MainActivity.class)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInBubble() {
|
||||
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
Display display = getDisplay();
|
||||
|
||||
return display != null && display.getDisplayId() != Display.DEFAULT_DISPLAY;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources(@NonNull ConversationIntents.Args args) {
|
||||
@@ -2498,7 +2540,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (refreshFragment) {
|
||||
fragment.reload(recipient.get(), threadId);
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
setVisibleThread(threadId);
|
||||
}
|
||||
|
||||
fragment.scrollToBottom();
|
||||
|
||||
@@ -481,9 +481,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId());
|
||||
this.threadId = conversationViewModel.getArgs().getThreadId();
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId());
|
||||
this.threadId = conversationViewModel.getArgs().getThreadId();
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
|
||||
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
|
||||
messageCountsViewModel.setThreadId(threadId);
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Objects;
|
||||
|
||||
public class ConversationIntents {
|
||||
|
||||
private static final String BUBBLE_AUTHORITY = "bubble";
|
||||
private static final String EXTRA_RECIPIENT = "recipient_id";
|
||||
private static final String EXTRA_THREAD_ID = "thread_id";
|
||||
private static final String EXTRA_TEXT = "draft_text";
|
||||
@@ -39,8 +40,20 @@ public class ConversationIntents {
|
||||
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId);
|
||||
}
|
||||
|
||||
public static @NonNull Intent createBubbleIntent(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
|
||||
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId).build();
|
||||
}
|
||||
|
||||
static boolean isInvalid(@NonNull Intent intent) {
|
||||
return !intent.hasExtra(EXTRA_RECIPIENT);
|
||||
if (isBubbleIntent(intent)) {
|
||||
return intent.getData().getQueryParameter(EXTRA_RECIPIENT) == null;
|
||||
} else {
|
||||
return !intent.hasExtra(EXTRA_RECIPIENT);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBubbleIntent(@NonNull Intent intent) {
|
||||
return intent.getData() != null && Objects.equals(intent.getData().getAuthority(), BUBBLE_AUTHORITY);
|
||||
}
|
||||
|
||||
final static class Args {
|
||||
@@ -54,6 +67,17 @@ public class ConversationIntents {
|
||||
private final int startingPosition;
|
||||
|
||||
static Args from(@NonNull Intent intent) {
|
||||
if (isBubbleIntent(intent)) {
|
||||
return new Args(RecipientId.from(intent.getData().getQueryParameter(EXTRA_RECIPIENT)),
|
||||
Long.parseLong(intent.getData().getQueryParameter(EXTRA_THREAD_ID)),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
-1);
|
||||
}
|
||||
|
||||
return new Args(RecipientId.from(Objects.requireNonNull(intent.getStringExtra(EXTRA_RECIPIENT))),
|
||||
intent.getLongExtra(EXTRA_THREAD_ID, -1),
|
||||
intent.getStringExtra(EXTRA_TEXT),
|
||||
@@ -197,6 +221,16 @@ public class ConversationIntents {
|
||||
Intent intent = new Intent(context, conversationActivityClass);
|
||||
|
||||
intent.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
if (Objects.equals(conversationActivityClass, BubbleConversationActivity.class)) {
|
||||
intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY)
|
||||
.appendQueryParameter(EXTRA_RECIPIENT, recipientId.serialize())
|
||||
.appendQueryParameter(EXTRA_THREAD_ID, String.valueOf(threadId))
|
||||
.build());
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
intent.putExtra(EXTRA_RECIPIENT, recipientId.serialize());
|
||||
intent.putExtra(EXTRA_THREAD_ID, threadId);
|
||||
intent.putExtra(EXTRA_DISTRIBUTION_TYPE, distributionType);
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -34,6 +39,17 @@ class ConversationRepository {
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
boolean canShowAsBubble(long threadId) {
|
||||
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
return recipient != null && BubbleUtil.canBubble(context, recipient.getId(), threadId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
|
||||
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
|
||||
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
|
||||
|
||||
@@ -39,6 +39,7 @@ class ConversationViewModel extends ViewModel {
|
||||
private final Invalidator invalidator;
|
||||
private final MutableLiveData<Boolean> showScrollButtons;
|
||||
private final MutableLiveData<Boolean> hasUnreadMentions;
|
||||
private final LiveData<Boolean> canShowAsBubble;
|
||||
|
||||
private ConversationIntents.Args args;
|
||||
private int jumpToPosition;
|
||||
@@ -94,6 +95,8 @@ class ConversationViewModel extends ViewModel {
|
||||
(m, data) -> new DistinctConversationDataByThreadId(data));
|
||||
|
||||
conversationMetadata = Transformations.map(Transformations.distinctUntilChanged(distinctData), DistinctConversationDataByThreadId::getConversationData);
|
||||
|
||||
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
@@ -113,6 +116,10 @@ class ConversationViewModel extends ViewModel {
|
||||
this.threadId.postValue(-1L);
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> canShowAsBubble() {
|
||||
return canShowAsBubble;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowScrollToBottom() {
|
||||
return Transformations.distinctUntilChanged(showScrollButtons);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user