Add support for Android 11 Conversation Bubbles.

This commit is contained in:
Alex Hart
2020-11-25 14:11:17 -04:00
committed by GitHub
parent 3aebadd90d
commit e1bf23251f
22 changed files with 518 additions and 70 deletions

View File

@@ -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 {
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}