Implement ShortcutInfo for API 30.
This commit is contained in:
parent
75062ada8a
commit
930370783e
18 changed files with 464 additions and 50 deletions
|
@ -100,7 +100,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||
|
||||
private void launch(Recipient recipient) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
||||
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ public class SmsSendtoActivity extends Activity {
|
|||
nextIntent = new Intent(this, ConversationActivity.class);
|
||||
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
||||
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
}
|
||||
return nextIntent;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
@ -242,6 +243,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
|
|||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
|
@ -413,12 +415,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
long threadId)
|
||||
{
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId.serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientId getRecipientId(@NonNull Intent intent) {
|
||||
return RecipientId.from(Objects.requireNonNull(intent.getStringExtra(RECIPIENT_EXTRA)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
|
@ -427,7 +434,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
|
||||
RecipientId recipientId = getRecipientId(getIntent());
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "[onCreate] Missing recipientId!");
|
||||
|
@ -437,7 +444,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
reportShortcutLaunch(recipientId);
|
||||
setContentView(R.layout.conversation_activity);
|
||||
|
||||
getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
|
||||
|
@ -505,7 +512,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
silentlySetComposeText("");
|
||||
}
|
||||
|
||||
RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
|
||||
RecipientId recipientId = getRecipientId(intent);
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "[onNewIntent] Missing recipientId!");
|
||||
|
@ -515,6 +522,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
return;
|
||||
}
|
||||
|
||||
reportShortcutLaunch(recipientId);
|
||||
setIntent(intent);
|
||||
initializeResources();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
|
@ -559,6 +567,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
|
||||
ConversationUtil.pushShortcutForRecipient(getApplicationContext(), recipientSnapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -743,6 +753,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
|
||||
}
|
||||
|
||||
private void reportShortcutLaunch(@NonNull RecipientId recipientId) {
|
||||
if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(this);
|
||||
if (shortcutManager != null) {
|
||||
shortcutManager.reportShortcutUsed(ConversationUtil.getShortcutId(recipientId));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleImageFromDeviceCameraApp() {
|
||||
if (attachmentManager.getCaptureUri() == null) {
|
||||
Log.w(TAG, "No image available.");
|
||||
|
@ -1955,7 +1976,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
recipient.removeObservers(this);
|
||||
}
|
||||
|
||||
recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient = Recipient.live(getRecipientId(getIntent()));
|
||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
||||
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
@ -1963,7 +1984,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
recipient.observe(this, this::onRecipientChanged);
|
||||
}
|
||||
|
||||
|
||||
private void initializeLinkPreviewObserver() {
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);
|
||||
|
||||
|
|
|
@ -480,7 +480,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
|
||||
this.recipient = Recipient.live(ConversationActivity.getRecipientId(requireActivity().getIntent()));
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ public class ConversationPopupActivity extends ConversationActivity {
|
|||
public void onSuccess(Long result) {
|
||||
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
|
||||
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId().serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
||||
|
||||
startActivity(intent, transition.toBundle());
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
|
@ -245,6 +246,7 @@ public class ThreadDatabase extends Database {
|
|||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
ConversationUtil.clearShortcuts(context, Collections.singleton(threadId));
|
||||
}
|
||||
|
||||
private void deleteThreads(Set<Long> threadIds) {
|
||||
|
@ -259,12 +261,14 @@ public class ThreadDatabase extends Database {
|
|||
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
notifyConversationListListeners();
|
||||
ConversationUtil.clearShortcuts(context, threadIds);
|
||||
}
|
||||
|
||||
private void deleteAllThreads() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
notifyConversationListListeners();
|
||||
ConversationUtil.clearAllShortcuts(context);
|
||||
}
|
||||
|
||||
public void trimAllThreads(int length, long trimBeforeDate) {
|
||||
|
@ -1194,6 +1198,10 @@ public class ThreadDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull ThreadRecord getThreadRecordFor(@NonNull Recipient recipient) {
|
||||
return Objects.requireNonNull(getThreadRecord(getThreadIdFor(recipient)));
|
||||
}
|
||||
|
||||
@NonNull MergeResult merge(@NonNull RecipientId primaryRecipientId, @NonNull RecipientId secondaryRecipientId) {
|
||||
if (!databaseHelper.getWritableDatabase().inTransaction()) {
|
||||
throw new IllegalStateException("Must be in a transaction!");
|
||||
|
|
|
@ -135,7 +135,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||
sendInThreadNotification(context, recipient);
|
||||
} else {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ public class NotificationState {
|
|||
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size());
|
||||
|
||||
Intent intent = new Intent(context, ConversationPopupActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
|
||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPrefere
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -88,6 +89,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||
setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal));
|
||||
setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context)));
|
||||
}
|
||||
|
||||
setShortcutId(ConversationUtil.getShortcutId(recipient));
|
||||
}
|
||||
|
||||
private Drawable getContactDrawable(@NonNull Recipient recipient) {
|
||||
|
@ -230,13 +233,14 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||
{
|
||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||
Person.Builder personBuilder = new Person.Builder()
|
||||
.setKey(individualRecipient.getId().serialize())
|
||||
.setKey(ConversationUtil.getShortcutId(individualRecipient))
|
||||
.setBot(false);
|
||||
|
||||
this.threadRecipient = threadRecipient;
|
||||
|
||||
if (privacy.isDisplayContact()) {
|
||||
personBuilder.setName(individualRecipient.getDisplayName(context));
|
||||
personBuilder.setUri(individualRecipient.isSystemContact() ? individualRecipient.getContactUri().toString() : null);
|
||||
|
||||
Bitmap bitmap = getLargeBitmap(getContactDrawable(individualRecipient));
|
||||
if (bitmap != null) {
|
||||
|
@ -283,13 +287,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||
}
|
||||
|
||||
private void applyMessageStyle() {
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(
|
||||
new Person.Builder()
|
||||
.setBot(false)
|
||||
.setName(Recipient.self().getDisplayName(context))
|
||||
.setKey(Recipient.self().getId().serialize())
|
||||
.setIcon(AvatarUtil.getIconForNotification(context, Recipient.self()))
|
||||
.build());
|
||||
ConversationUtil.pushShortcutForRecipientIfNeededSync(context, threadRecipient);
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self()));
|
||||
|
||||
if (threadRecipient.isGroup()) {
|
||||
if (privacy.isDisplayContact()) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.jobs.StorageForcePushJob;
|
|||
import org.thoughtcrime.securesms.keyvalue.InternalValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
|
||||
public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
private static final String TAG = Log.tag(InternalOptionsPreferenceFragment.class);
|
||||
|
@ -69,6 +70,12 @@ public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragme
|
|||
Toast.makeText(getContext(), "Scheduled storage force push", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
|
||||
findPreference("pref_delete_dynamic_shortcuts").setOnPreferenceClickListener(preference -> {
|
||||
ConversationUtil.clearAllShortcuts(requireContext());
|
||||
Toast.makeText(getContext(), "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeSwitchPreference(@NonNull PreferenceDataStore preferenceDataStore,
|
||||
|
|
|
@ -4,51 +4,78 @@ package org.thoughtcrime.securesms.service;
|
|||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.chooser.ChooserTarget;
|
||||
import android.service.chooser.ChooserTargetService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
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.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class DirectShareService extends ChooserTargetService {
|
||||
|
||||
|
||||
private static final String TAG = DirectShareService.class.getSimpleName();
|
||||
private static final int MAX_TARGETS = 10;
|
||||
|
||||
@Override
|
||||
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName,
|
||||
IntentFilter matchedFilter)
|
||||
{
|
||||
List<ChooserTarget> results = new LinkedList<>();
|
||||
Map<RecipientId, ChooserTarget> results = new LinkedHashMap<>();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(this);
|
||||
if (shortcutManager != null && !shortcutManager.getDynamicShortcuts().isEmpty()) {
|
||||
addChooserTargetsFromDynamicShortcuts(results, shortcutManager.getDynamicShortcuts());
|
||||
}
|
||||
|
||||
if (results.size() >= MAX_TARGETS) {
|
||||
return new ArrayList<>(results.values());
|
||||
}
|
||||
}
|
||||
|
||||
ComponentName componentName = new ComponentName(this, ShareActivity.class);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
|
||||
Cursor cursor = threadDatabase.getRecentConversationList(10, false, FeatureFlags.groupsV1ForcedMigration());
|
||||
|
||||
try {
|
||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentConversationList(MAX_TARGETS, false, FeatureFlags.groupsV1ForcedMigration()))) {
|
||||
ThreadRecord record;
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (results.containsKey(record.getRecipient().getId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
|
||||
String name = recipient.getDisplayName(this);
|
||||
|
||||
|
@ -71,19 +98,23 @@ public class DirectShareService extends ChooserTargetService {
|
|||
avatar = getFallbackDrawable(recipient);
|
||||
}
|
||||
|
||||
Bundle bundle = buildExtras(record);
|
||||
|
||||
results.put(recipient.getId(), new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
|
||||
}
|
||||
|
||||
return new ArrayList<>(results.values());
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Bundle buildExtras(@NonNull ThreadRecord threadRecord) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId());
|
||||
bundle.putString(ShareActivity.EXTRA_RECIPIENT_ID, recipient.getId().serialize());
|
||||
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType());
|
||||
bundle.setClassLoader(getClassLoader());
|
||||
|
||||
results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
|
||||
}
|
||||
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, threadRecord.getThreadId());
|
||||
bundle.putString(ShareActivity.EXTRA_RECIPIENT_ID, threadRecord.getRecipient().getId().serialize());
|
||||
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, threadRecord.getDistributionType());
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private Bitmap getFallbackDrawable(@NonNull Recipient recipient) {
|
||||
|
@ -92,4 +123,28 @@ public class DirectShareService extends ChooserTargetService {
|
|||
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height));
|
||||
}
|
||||
|
||||
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
|
||||
private void addChooserTargetsFromDynamicShortcuts(@NonNull Map<RecipientId, ChooserTarget> targetMap, @NonNull List<ShortcutInfo> shortcutInfos) {
|
||||
Stream.of(shortcutInfos)
|
||||
.sorted((lhs, rhs) -> Integer.compare(lhs.getRank(), rhs.getRank()))
|
||||
.takeWhileIndexed((idx, info) -> idx < MAX_TARGETS)
|
||||
.forEach(info -> {
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(info.getId()));
|
||||
ChooserTarget target = buildChooserTargetFromShortcutInfo(info, recipient);
|
||||
|
||||
targetMap.put(RecipientId.from(info.getId()), target);
|
||||
});
|
||||
}
|
||||
|
||||
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
|
||||
private @NonNull ChooserTarget buildChooserTargetFromShortcutInfo(@NonNull ShortcutInfo info, @NonNull Recipient recipient) {
|
||||
ThreadRecord threadRecord = DatabaseFactory.getThreadDatabase(this).getThreadRecordFor(recipient);
|
||||
|
||||
return new ChooserTarget(info.getShortLabel(),
|
||||
AvatarUtil.getIconForShortcut(this, recipient),
|
||||
info.getRank() / ((float) MAX_TARGETS),
|
||||
new ComponentName(this, ShareActivity.class),
|
||||
buildExtras(threadRecord));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -325,7 +325,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
|||
Log.i(TAG, "Shared data was not external.");
|
||||
}
|
||||
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId.serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util;
|
|||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
@ -10,6 +11,7 @@ import android.widget.ImageView;
|
|||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
@ -22,8 +24,11 @@ import com.bumptech.glide.request.transition.Transition;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -33,6 +38,8 @@ import java.util.concurrent.ExecutionException;
|
|||
|
||||
public final class AvatarUtil {
|
||||
|
||||
private static final String TAG = Log.tag(AvatarUtil.class);
|
||||
|
||||
private AvatarUtil() {
|
||||
}
|
||||
|
||||
|
@ -93,6 +100,16 @@ public final class AvatarUtil {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
public static Icon getIconForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
try {
|
||||
return Icon.createWithAdaptiveBitmap(getShortcutInfoBitmap(context, recipient));
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
return Icon.createWithAdaptiveBitmap(getFallbackForShortcut(context, recipient));
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
try {
|
||||
|
@ -102,13 +119,8 @@ public final class AvatarUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static GlideRequest<Drawable> getSelfAvatarOrFallbackIcon(@NonNull Context context, @DrawableRes int fallbackIcon) {
|
||||
return GlideApp.with(context)
|
||||
.asDrawable()
|
||||
.load(new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar()))
|
||||
.error(fallbackIcon)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||
private static @NonNull Bitmap getShortcutInfoBitmap(@NonNull Context context, @NonNull Recipient recipient) throws ExecutionException, InterruptedException {
|
||||
return DrawableUtil.wrapBitmapForShortcutInfo(request(GlideApp.with(context).asBitmap(), context, recipient, false).circleCrop().submit().get());
|
||||
}
|
||||
|
||||
private static <T> GlideRequest<T> requestCircle(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
|
||||
|
@ -120,11 +132,40 @@ public final class AvatarUtil {
|
|||
}
|
||||
|
||||
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
|
||||
return glideRequest.load(new ProfileContactPhoto(recipient, recipient.getProfileAvatar()))
|
||||
return request(glideRequest, context, recipient, true);
|
||||
}
|
||||
|
||||
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf) {
|
||||
final ContactPhoto photo;
|
||||
if (Recipient.self().equals(recipient) && loadSelf) {
|
||||
photo = new ProfileContactPhoto(recipient, recipient.getProfileAvatar());
|
||||
} else {
|
||||
photo = recipient.getContactPhoto();
|
||||
}
|
||||
|
||||
return glideRequest.load(photo)
|
||||
.error(getFallback(context, recipient))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||
}
|
||||
|
||||
private static @NonNull Bitmap getFallbackForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
@DrawableRes final int photoSource;
|
||||
if (recipient.isSelf()) {
|
||||
photoSource = R.drawable.ic_note_80;
|
||||
} else if (recipient.isGroup()) {
|
||||
photoSource = R.drawable.ic_group_80;
|
||||
} else {
|
||||
photoSource = R.drawable.ic_profile_80;
|
||||
}
|
||||
|
||||
Bitmap toWrap = DrawableUtil.toBitmap(new FallbackPhoto80dp(photoSource, recipient.getColor()).asDrawable(context, -1), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
|
||||
Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap);
|
||||
|
||||
toWrap.recycle();
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(context)).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.Person;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* ConversationUtil encapsulates support for Android 11+'s new Conversations system
|
||||
*/
|
||||
public final class ConversationUtil {
|
||||
|
||||
public static final int CONVERSATION_SUPPORT_VERSION = 30;
|
||||
|
||||
private ConversationUtil() {}
|
||||
|
||||
/**
|
||||
* Pushes a new dynamic shortcut for the given recipient and updates the ranks of all current
|
||||
* shortcuts.
|
||||
*/
|
||||
public static void pushShortcutForRecipient(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
pushShortcutAndUpdateRanks(context, recipient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously pushes a new dynamic shortcut for the given recipient if one does not already exist.
|
||||
*
|
||||
* If added, this recipient is given a high ranking with the intention of not appearing immediately in results.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void pushShortcutForRecipientIfNeededSync(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
|
||||
String shortcutId = getShortcutId(recipient);
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
||||
|
||||
boolean hasPushedRecipientShortcut = Stream.of(shortcuts)
|
||||
.filter(info -> Objects.equals(shortcutId, info.getId()))
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
|
||||
if (!hasPushedRecipientShortcut) {
|
||||
pushShortcutForRecipientInternal(context, recipient, shortcuts.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all currently set dynamic shortcuts
|
||||
*/
|
||||
public static void clearAllShortcuts(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
|
||||
List<ShortcutInfo> shortcutInfos = shortcutManager.getDynamicShortcuts();
|
||||
|
||||
shortcutManager.removeLongLivedShortcuts(Stream.of(shortcutInfos).map(ShortcutInfo::getId).toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the shortcuts tied to a given thread.
|
||||
*/
|
||||
public static void clearShortcuts(@NonNull Context context, @NonNull Set<Long> threadIds) {
|
||||
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<RecipientId> recipientIds = DatabaseFactory.getThreadDatabase(context).getRecipientIdsForThreadIds(threadIds);
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
|
||||
|
||||
shortcutManager.removeLongLivedShortcuts(Stream.of(recipientIds).map(ConversationUtil::getShortcutId).toList());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID that is unique between all recipients.
|
||||
*
|
||||
* @param recipientId The recipient ID to get a shortcut ID for
|
||||
*
|
||||
* @return A unique identifier that is stable for a given recipient id
|
||||
*/
|
||||
public static @NonNull String getShortcutId(@NonNull RecipientId recipientId) {
|
||||
return recipientId.serialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID that is unique between all recipients.
|
||||
*
|
||||
* @param recipient The recipient to get a shortcut for.
|
||||
*
|
||||
* @return A unique identifier that is stable for a given recipient id
|
||||
*/
|
||||
public static @NonNull String getShortcutId(@NonNull Recipient recipient) {
|
||||
return getShortcutId(recipient.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rank of each existing shortcut by 1 and then publishes a new shortcut of rank 0
|
||||
* for the given recipient.
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static void pushShortcutAndUpdateRanks(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
|
||||
List<ShortcutInfo> currentShortcuts = shortcutManager.getDynamicShortcuts();
|
||||
|
||||
if (Util.isEmpty(currentShortcuts)) {
|
||||
for (ShortcutInfo shortcutInfo : currentShortcuts) {
|
||||
RecipientId recipientId = RecipientId.from(shortcutInfo.getId());
|
||||
Recipient resolved = Recipient.resolved(recipientId);
|
||||
ShortcutInfo updated = buildShortcutInfo(context, resolved, shortcutInfo.getRank() + 1);
|
||||
|
||||
shortcutManager.pushDynamicShortcut(updated);
|
||||
}
|
||||
}
|
||||
|
||||
pushShortcutForRecipientInternal(context, recipient, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a dynamic shortcut for a given recipient to the shortcut manager
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static void pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank) {
|
||||
ShortcutInfo shortcutInfo = buildShortcutInfo(context, recipient, rank);
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
|
||||
|
||||
shortcutManager.pushDynamicShortcut(shortcutInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the shortcut info object for a given Recipient.
|
||||
*
|
||||
* @param context The Context under which we are operating
|
||||
* @param recipient The Recipient to generate a ShortcutInfo for
|
||||
* @param rank The rank that should be assigned to this recipient
|
||||
* @return The new ShortcutInfo
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static @NonNull ShortcutInfo buildShortcutInfo(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
int rank)
|
||||
{
|
||||
Recipient resolved = recipient.resolve();
|
||||
Person[] persons = buildPersons(context, resolved);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
|
||||
String shortName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getShortDisplayName(context);
|
||||
String longName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getDisplayName(context);
|
||||
|
||||
return new ShortcutInfo.Builder(context, getShortcutId(resolved))
|
||||
.setLongLived(true)
|
||||
.setIntent(ConversationActivity.buildIntent(context, resolved.getId(), threadId))
|
||||
.setShortLabel(shortName)
|
||||
.setLongLabel(longName)
|
||||
.setIcon(AvatarUtil.getIconForShortcut(context, resolved))
|
||||
.setPersons(persons)
|
||||
.setCategories(Collections.singleton(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION))
|
||||
.setActivity(new ComponentName(context, "org.thoughtcrime.securesms.RoutingActivity"))
|
||||
.setRank(rank)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an array of Person objects correlating to members of a conversation (other than self)
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static @NonNull Person[] buildPersons(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (recipient.isGroup()) {
|
||||
return buildPersonsForGroup(context, recipient.getGroupId().get());
|
||||
} else {
|
||||
return new Person[]{buildPerson(context, recipient)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an array of Person objects correlating to members of a group (other than self)
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static @NonNull Person[] buildPersonsForGroup(@NonNull Context context, @NonNull GroupId groupId) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
return Stream.of(members).map(member -> buildPerson(context, member.resolve())).toArray(Person[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A Person object representing the given Recipient
|
||||
*/
|
||||
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
|
||||
@WorkerThread
|
||||
private static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return new Person.Builder()
|
||||
.setKey(getShortcutId(recipient.getId()))
|
||||
.setName(recipient.getDisplayName(context))
|
||||
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A Compat Library Person object representing the given Recipient
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull androidx.core.app.Person buildPersonCompat(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return new androidx.core.app.Person.Builder()
|
||||
.setKey(getShortcutId(recipient.getId()))
|
||||
.setName(recipient.getDisplayName(context))
|
||||
.setIcon(AvatarUtil.getIconForNotification(context, recipient))
|
||||
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -6,10 +6,15 @@ import android.graphics.drawable.Drawable;
|
|||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.impl.SingleImageProxyBundle;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public final class DrawableUtil {
|
||||
|
||||
private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108);
|
||||
private static final int SHORTCUT_INFO_WRAPPED_SIZE = ViewUtil.dpToPx(72);
|
||||
private static final int SHORTCUT_INFO_PADDING = (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2;
|
||||
|
||||
private DrawableUtil() {}
|
||||
|
||||
public static @NonNull Bitmap toBitmap(@NonNull Drawable drawable, int width, int height) {
|
||||
|
@ -22,6 +27,16 @@ public final class DrawableUtil {
|
|||
return bitmap;
|
||||
}
|
||||
|
||||
public static @NonNull Bitmap wrapBitmapForShortcutInfo(@NonNull Bitmap toWrap) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
|
||||
Bitmap scaled = Bitmap.createScaledBitmap(toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true);
|
||||
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
canvas.drawBitmap(scaled, SHORTCUT_INFO_PADDING, SHORTCUT_INFO_PADDING, null);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.app.NotificationManager;
|
|||
import android.app.job.JobScheduler;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.hardware.SensorManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.location.LocationManager;
|
||||
|
@ -31,6 +32,11 @@ public class ServiceUtil {
|
|||
return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
@RequiresApi(25)
|
||||
public static @Nullable ShortcutManager getShortcutManager(@NonNull Context context) {
|
||||
return ContextCompat.getSystemService(context, ShortcutManager.class);
|
||||
}
|
||||
|
||||
public static WindowManager getWindowManager(Context context) {
|
||||
return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE);
|
||||
}
|
||||
|
|
|
@ -2386,6 +2386,9 @@
|
|||
<string name="preferences__internal_network" translatable="false">Network</string>
|
||||
<string name="preferences__internal_force_censorship" translatable="false">Force censorship</string>
|
||||
<string name="preferences__internal_force_censorship_description" translatable="false">Force the app to behave as if it is in a country where Signal is censored.</string>
|
||||
<string name="preferences__internal_conversations_and_shortcuts">Conversations and Shortcuts</string>
|
||||
<string name="preferences__internal_delete_all_dynamic_shortcuts">Delete all dynamic shortcuts</string>
|
||||
<string name="preferences__internal_click_to_delete_all_dynamic_shortcuts">Click to delete all dynamic shortcuts</string>
|
||||
|
||||
<!-- **************************************** -->
|
||||
<!-- menus -->
|
||||
|
|
|
@ -104,4 +104,25 @@
|
|||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/preferences__internal_network">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="internal.force_censorship"
|
||||
android:summary="@string/preferences__internal_force_censorship_description"
|
||||
android:title="@string/preferences__internal_force_censorship" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/preferences__internal_conversations_and_shortcuts">
|
||||
|
||||
<Preference
|
||||
android:key="pref_delete_dynamic_shortcuts"
|
||||
android:title="@string/preferences__internal_delete_all_dynamic_shortcuts"
|
||||
android:summary="@string/preferences__internal_click_to_delete_all_dynamic_shortcuts" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
Loading…
Add table
Reference in a new issue