Skip to content

Commit

Permalink
Add support for Android 11 Conversation Bubbles.
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-signal authored Nov 25, 2020
1 parent 3aebadd commit e1bf232
Show file tree
Hide file tree
Showing 22 changed files with 518 additions and 70 deletions.
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>

<activity android:name=".conversation.BubbleConversationActivity"
android:theme="@style/Signal.DayNight"
android:allowEmbedded="true"
android:resizeableActivity="true" />

<activity android:name=".longmessage.LongMessageActivity" />

<activity android:name=".conversation.ConversationPopupActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
import org.thoughtcrime.securesms.notifications.NotificationChannels;

import java.io.IOException;
Expand All @@ -34,7 +35,7 @@ public enum BackupFileIOError {
}

public static void clearNotification(@NonNull Context context) {
NotificationManagerCompat.from(context).cancel(BACKUP_FAILED_ID);
NotificationCancellationHelper.cancelLegacy(context, BACKUP_FAILED_ID);
}

public void postNotification(@NonNull Context context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
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;
Expand Down Expand Up @@ -241,6 +242,7 @@
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;
Expand Down Expand Up @@ -530,15 +532,17 @@ protected void onResume() {
.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();

Expand Down Expand Up @@ -717,6 +721,12 @@ protected void onRestoreInstanceState(Bundle savedInstanceState) {
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;
Expand Down Expand Up @@ -870,6 +880,15 @@ public boolean onCreateOptionsMenu(Menu menu) {
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();
Expand Down Expand Up @@ -948,6 +967,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
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;
}

Expand Down Expand Up @@ -1220,6 +1240,13 @@ public void onLoadCleared(@Nullable Drawable placeholder) {

}

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)
Expand Down Expand Up @@ -1933,6 +1960,21 @@ protected void initializeActionBar() {

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) {
Expand Down Expand Up @@ -2498,7 +2540,7 @@ protected void sendComplete(long threadId) {

if (refreshFragment) {
fragment.reload(recipient.get(), threadId);
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
setVisibleThread(threadId);
}

fragment.scrollToBottom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,9 +481,9 @@ private void initializeResources() {

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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";
Expand All @@ -39,8 +40,20 @@ private 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 {
Expand All @@ -54,6 +67,17 @@ final static class Args {
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),
Expand Down Expand Up @@ -197,6 +221,16 @@ private Builder(@NonNull Context context,
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -34,6 +39,17 @@ LiveData<ConversationData> getConversationData(long threadId, int jumpToPosition
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,6 +95,8 @@ private ConversationViewModel() {
(m, data) -> new DistinctConversationDataByThreadId(data));

conversationMetadata = Transformations.map(Transformations.distinctUntilChanged(distinctData), DistinctConversationDataByThreadId::getConversationData);

canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
}

void onAttachmentKeyboardOpen() {
Expand All @@ -113,6 +116,10 @@ void clearThreadId() {
this.threadId.postValue(-1L);
}

@NonNull LiveData<Boolean> canShowAsBubble() {
return canShowAsBubble;
}

@NonNull LiveData<Boolean> getShowScrollToBottom() {
return Transformations.distinctUntilChanged(showScrollButtons);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package org.thoughtcrime.securesms.jobs;


import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.documentfile.provider.DocumentFile;

import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupFileIOError;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import androidx.core.app.NotificationManagerCompat;

import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;

import java.util.LinkedList;
Expand All @@ -53,7 +52,7 @@ public void onReceive(final Context context, Intent intent)

if (threadIds != null) {
int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1);
NotificationManagerCompat.from(context).cancel(notificationId);
NotificationCancellationHelper.cancelLegacy(context, notificationId);

new AsyncTask<Void, Void, Void>() {
@Override
Expand Down
Loading

2 comments on commit e1bf232

@ShowcaseKing
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you support chat bubbles on older versions of android (specifically android 7) or is this just a feature for android 11?

@greyson-signal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShowcaseKing Nope, this is just for Android 11, which has built-in support for chat bubbles.

Please sign in to comment.