WIP: clean up signal protocols (stickers)

This commit is contained in:
Ryan ZHAO 2021-02-22 10:06:40 +11:00
parent 04f140ee09
commit 19a829d011
49 changed files with 28 additions and 3791 deletions

View File

@ -62,7 +62,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.logging.PersistentLogger;

View File

@ -36,6 +36,5 @@ public interface BindableConversationItem extends Unbindable {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator);
}
}

View File

@ -86,12 +86,6 @@ object FullBackupExporter {
},
count)
}
StickerDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ true },
{ cursor: Cursor -> exportSticker(attachmentSecret, cursor, outputStream) },
count)
}
else -> {
exportTable(table, input, outputStream, null, null, count)
}
@ -236,20 +230,6 @@ object FullBackupExporter {
}
}
private fun exportSticker(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
try {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH))
val data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH))
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM))
if (!TextUtils.isEmpty(data) && size > 0) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0).use { inputStream -> outputStream.writeSticker(rowId, inputStream, size) }
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
@Throws(IOException::class)
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
var result: Long = 0

View File

@ -67,7 +67,6 @@ object FullBackupImporter {
frame.hasStatement() -> processStatement(db, frame.statement)
frame.hasPreference() -> processPreference(context, frame.preference)
frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
frame.hasSticker() -> processSticker(context, attachmentSecret, db, frame.sticker, inputStream)
frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
}
}
@ -132,21 +131,6 @@ object FullBackupImporter {
arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
}
@Throws(IOException::class)
private fun processSticker(context: Context, attachmentSecret: AttachmentSecret,
db: SQLiteDatabase, sticker: Sticker,
inputStream: BackupRecordInputStream) {
val stickerDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
val dataFile = File.createTempFile("sticker", ".mms", stickerDirectory)
val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
inputStream.readAttachmentTo(output.second, sticker.length)
val contentValues = ContentValues()
contentValues.put(StickerDatabase.FILE_PATH, dataFile.absolutePath)
contentValues.put(StickerDatabase.FILE_RANDOM, output.first)
db.update(StickerDatabase.TABLE_NAME, contentValues,
StickerDatabase._ID + " = ?", arrayOf(sticker.rowId.toString()))
}
@Throws(IOException::class)
private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
inputStream.readAttachmentTo(FileOutputStream(

View File

@ -7,8 +7,6 @@ import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageButton;
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
import org.session.libsession.utilities.TextSecurePreferences;
import network.loki.messenger.R;
@ -82,8 +80,5 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
@Override
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
setStickerMode(provider instanceof StickerKeyboardProvider);
TextSecurePreferences.setMediaKeyboardMode(getContext(), (provider instanceof StickerKeyboardProvider) ? TextSecurePreferences.MediaKeyboardMode.STICKER
: TextSecurePreferences.MediaKeyboardMode.EMOJI);
}
}

View File

@ -182,10 +182,6 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
@ -205,7 +201,6 @@ import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsignal.utilities.concurrent.ListenableFuture;
import org.session.libsignal.utilities.concurrent.SettableFuture;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.TextSecurePreferences.MediaKeyboardMode;
import java.io.IOException;
import java.text.SimpleDateFormat;
@ -239,7 +234,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
InputPanel.MediaListener,
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener,
StickerKeyboardProvider.StickerEventListener,
LokiThreadDatabaseDelegate
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@ -303,7 +297,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
private ConversationStickerViewModel stickerViewModel;
private Recipient recipient;
private long threadId;
@ -361,7 +354,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeResources();
initializeLinkPreviewObserver();
initializeSearchObserver();
initializeStickerObserver();
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@ -1513,52 +1505,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
private void initializeStickerObserver() {
StickerSearchRepository repository = new StickerSearchRepository(this);
stickerViewModel = ViewModelProviders.of(this, new ConversationStickerViewModel.Factory(getApplication(), repository))
.get(ConversationStickerViewModel.class);
stickerViewModel.getStickerResults().observe(this, stickers -> {
if (stickers == null) return;
inputPanel.setStickerSuggestions(stickers);
});
stickerViewModel.getStickersAvailability().observe(this, stickersAvailable -> {
if (stickersAvailable == null) return;
boolean isSystemEmojiPreferred = TextSecurePreferences.isSystemEmojiPreferred(this);
MediaKeyboardMode keyboardMode = TextSecurePreferences.getMediaKeyboardMode(this);
boolean stickerIntro = !TextSecurePreferences.hasSeenStickerIntroTooltip(this);
if (stickersAvailable) {
inputPanel.showMediaKeyboardToggle(true);
inputPanel.setMediaKeyboardToggleMode(isSystemEmojiPreferred || keyboardMode == MediaKeyboardMode.STICKER);
if (stickerIntro) showStickerIntroductionTooltip();
}
if (emojiDrawerStub.resolved()) {
initializeMediaKeyboardProviders(emojiDrawerStub.get(), stickersAvailable);
}
});
}
private void showStickerIntroductionTooltip() {
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
inputPanel.setMediaKeyboardToggleMode(true);
TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
.setBackgroundTint(getResources().getColor(R.color.core_blue))
.setTextColor(getResources().getColor(R.color.core_white))
.setText(R.string.ConversationActivity_new_say_it_with_stickers)
.setOnDismissListener(() -> {
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
EventBus.getDefault().removeStickyEvent(StickerPackInstallEvent.class);
})
.show(TooltipPopup.POSITION_ABOVE);
}
@Override
public void onSearchMoveUpPressed() {
searchViewModel.onMoveUp();
@ -1588,17 +1534,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onStickerPackInstalled(final StickerPackInstallEvent event) {
if (!TextSecurePreferences.hasSeenStickerIntroTooltip(this)) return;
EventBus.getDefault().removeStickyEvent(event);
TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
.setText(R.string.ConversationActivity_sticker_pack_installed)
.setIconGlideModel(event.getIconGlideModel())
.show(TooltipPopup.POSITION_ABOVE);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
@ -1813,21 +1748,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard, boolean stickersAvailable) {
private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard) {
boolean isSystemEmojiPreferred = TextSecurePreferences.isSystemEmojiPreferred(this);
if (stickersAvailable) {
if (isSystemEmojiPreferred) {
mediaKeyboard.setProviders(0, new StickerKeyboardProvider(this, this));
} else {
MediaKeyboardMode keyboardMode = TextSecurePreferences.getMediaKeyboardMode(this);
int index = keyboardMode == MediaKeyboardMode.STICKER ? 1 : 0;
mediaKeyboard.setProviders(index,
new EmojiKeyboardProvider(this, inputPanel),
new StickerKeyboardProvider(this, this));
}
} else if (!isSystemEmojiPreferred) {
if (!isSystemEmojiPreferred) {
mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, inputPanel));
}
}
@ -2230,9 +2153,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onEmojiToggle() {
if (!emojiDrawerStub.resolved()) {
Boolean stickersAvailable = stickerViewModel.getStickersAvailability().getValue();
initializeMediaKeyboardProviders(emojiDrawerStub.get(), stickersAvailable == null ? false : stickersAvailable);
initializeMediaKeyboardProviders(emojiDrawerStub.get());
inputPanel.setMediaKeyboard(emojiDrawerStub.get());
}
@ -2272,23 +2193,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
}
@Override
public void onStickerSelected(@NonNull StickerRecord stickerRecord) {
sendSticker(stickerRecord, false);
}
@Override
public void onStickerManagementClicked() {
startActivity(StickerManagementActivity.getIntent(this));
container.hideAttachedInput(true);
}
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId()), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
DatabaseFactory.getStickerDatabase(this).updateStickerLastUsedTime(stickerRecord.getRowId(), System.currentTimeMillis());
});
}
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
@ -2404,12 +2310,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void afterTextChanged(Editable s) {
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
}
stickerViewModel.onInputTextUpdated(s.toString());
}
@Override

View File

@ -85,7 +85,6 @@ import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
@ -1084,13 +1083,6 @@ public class ConversationFragment extends Fragment
startActivity(LongMessageActivity.getIntent(getContext(), conversationAddress, messageId, isMms));
}
}
@Override
public void onStickerClicked(@NonNull StickerLocator sticker) {
if (getContext() != null && getActivity() != null) {
startActivity(StickerPackPreviewActivity.getIntent(sticker.getPackId(), sticker.getPackKey()));
}
}
}
private class ActionModeCallback implements ActionMode.Callback {

View File

@ -96,7 +96,6 @@ import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
@ -460,8 +459,7 @@ public class ConversationItem extends LinearLayout
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width);
return linkPreview.getThumbnail().isPresent() &&
linkPreview.getThumbnail().get().getWidth() >= minWidth &&
!StickerUrl.isValidShareLink(linkPreview.getUrl());
linkPreview.getThumbnail().get().getWidth() >= minWidth;
}
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery, boolean isGroupThread) {
@ -1154,9 +1152,6 @@ public class ConversationItem extends LinearLayout
public void onClick(View v, Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (eventListener != null && hasSticker(messageRecord)){
//noinspection ConstantConditions
eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().getSticker());
}
}
}

View File

@ -1,85 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.session.libsession.utilities.Throttler;
class ConversationStickerViewModel extends ViewModel {
private static final int SEARCH_LIMIT = 10;
private final Application application;
private final StickerSearchRepository repository;
private final CloseableLiveData<CursorList<StickerRecord>> stickers;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final ContentObserver packObserver;
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
this.stickers = new CloseableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
}
};
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
}
@NonNull LiveData<CursorList<StickerRecord>> getStickerResults() {
return stickers;
}
@NonNull LiveData<Boolean> getStickersAvailability() {
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
return stickersAvailable;
}
void onInputTextUpdated(@NonNull String text) {
if (TextUtils.isEmpty(text) || text.length() > SEARCH_LIMIT) {
stickers.setValue(CursorList.emptyList());
} else {
repository.searchByEmoji(text, stickers::postValue);
}
}
@Override
protected void onCleared() {
stickers.close();
application.getContentResolver().unregisterContentObserver(packObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerSearchRepository repository;
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationStickerViewModel(application, repository));
}
}
}

View File

@ -548,18 +548,6 @@ public class AttachmentDatabase extends Database {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
/**
* Returns (pack_id, pack_key) pairs that are referenced in attachments but not in the stickers
* database.
*/
public @Nullable Cursor getUnavailableStickerPacks() {
String query = "SELECT DISTINCT " + STICKER_PACK_ID + ", " + STICKER_PACK_KEY + " FROM " + TABLE_NAME + " WHERE " + STICKER_PACK_ID + " NOT IN (" +
"SELECT DISTINCT " + StickerDatabase.PACK_ID + " FROM " + StickerDatabase.TABLE_NAME +
")";
return databaseHelper.getReadableDatabase().rawQuery(query, null);
}
public boolean hasStickerAttachments() {
String selection = STICKER_PACK_ID + " NOT NULL";

View File

@ -55,7 +55,6 @@ public class DatabaseFactory {
private final GroupReceiptDatabase groupReceiptDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
private final StickerDatabase stickerDatabase;
// Loki
private final LokiAPIDatabase lokiAPIDatabase;
@ -130,10 +129,6 @@ public class DatabaseFactory {
return getInstance(context).jobDatabase;
}
public static StickerDatabase getStickerDatabase(Context context) {
return getInstance(context).stickerDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase();
}
@ -199,7 +194,6 @@ public class DatabaseFactory {
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper);
this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper);
this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper);

View File

@ -1,481 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.IncomingSticker;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
import org.session.libsession.utilities.Util;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StickerDatabase extends Database {
private static final String TAG = Log.tag(StickerDatabase.class);
public static final String TABLE_NAME = "sticker";
public static final String _ID = "_id";
static final String PACK_ID = "pack_id";
private static final String PACK_KEY = "pack_key";
private static final String PACK_TITLE = "pack_title";
private static final String PACK_AUTHOR = "pack_author";
private static final String STICKER_ID = "sticker_id";
private static final String EMOJI = "emoji";
private static final String COVER = "cover";
private static final String INSTALLED = "installed";
private static final String LAST_USED = "last_used";
public static final String FILE_PATH = "file_path";
public static final String FILE_LENGTH = "file_length";
public static final String FILE_RANDOM = "file_random";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
PACK_ID + " TEXT NOT NULL, " +
PACK_KEY + " TEXT NOT NULL, " +
PACK_TITLE + " TEXT NOT NULL, " +
PACK_AUTHOR + " TEXT NOT NULL, " +
STICKER_ID + " INTEGER, " +
COVER + " INTEGER, " +
EMOJI + " TEXT NOT NULL, " +
LAST_USED + " INTEGER, " +
INSTALLED + " INTEGER," +
FILE_PATH + " TEXT NOT NULL, " +
FILE_LENGTH + " INTEGER, " +
FILE_RANDOM + " BLOB, " +
"UNIQUE(" + PACK_ID + ", " + STICKER_ID + ", " + COVER + ") ON CONFLICT IGNORE)";
public static final String[] CREATE_INDEXES = {
"CREATE INDEX IF NOT EXISTS sticker_pack_id_index ON " + TABLE_NAME + " (" + PACK_ID + ");",
"CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON " + TABLE_NAME + " (" + STICKER_ID + ");"
};
private static final String DIRECTORY = "stickers";
private final AttachmentSecret attachmentSecret;
public StickerDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
super(context, databaseHelper);
this.attachmentSecret = attachmentSecret;
}
public void insertSticker(@NonNull IncomingSticker sticker, @NonNull InputStream dataStream) throws IOException {
FileInfo fileInfo = saveStickerImage(dataStream);
ContentValues contentValues = new ContentValues();
contentValues.put(PACK_ID, sticker.getPackId());
contentValues.put(PACK_KEY, sticker.getPackKey());
contentValues.put(PACK_TITLE, sticker.getPackTitle());
contentValues.put(PACK_AUTHOR, sticker.getPackAuthor());
contentValues.put(STICKER_ID, sticker.getStickerId());
contentValues.put(EMOJI, sticker.getEmoji());
contentValues.put(COVER, sticker.isCover() ? 1 : 0);
contentValues.put(INSTALLED, sticker.isInstalled() ? 1 : 0);
contentValues.put(FILE_PATH, fileInfo.getFile().getAbsolutePath());
contentValues.put(FILE_LENGTH, fileInfo.getLength());
contentValues.put(FILE_RANDOM, fileInfo.getRandom());
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
if (id > 0) {
notifyStickerListeners();
if (sticker.isCover()) {
notifyStickerPackListeners();
if (sticker.isInstalled()) {
broadcastInstallEvent(sticker.getPackId());
}
}
}
}
public @Nullable StickerRecord getSticker(@NonNull String packId, int stickerId, boolean isCover) {
String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?";
String[] args = new String[] { packId, String.valueOf(stickerId), String.valueOf(isCover ? 1 : 0) };
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
return new StickerRecordReader(cursor).getNext();
}
}
public @Nullable StickerPackRecord getStickerPack(@NonNull String packId) {
String query = PACK_ID + " = ? AND " + COVER + " = ?";
String[] args = new String[] { packId, "1" };
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, "1")) {
return new StickerPackRecordReader(cursor).getNext();
}
}
public @Nullable Cursor getInstalledStickerPacks() {
String selection = COVER + " = ? AND " + INSTALLED + " = ?";
String[] args = new String[] { "1", "1" };
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null);
setNotifyStickerPackListeners(cursor);
return cursor;
}
public @Nullable Cursor getStickersByEmoji(@NonNull String emoji) {
String selection = EMOJI + " LIKE ? AND " + COVER + " = ?";
String[] args = new String[] { "%"+emoji+"%", "0" };
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null);
setNotifyStickerListeners(cursor);
return cursor;
}
public @Nullable Cursor getAllStickerPacks() {
return getAllStickerPacks(null);
}
public @Nullable Cursor getAllStickerPacks(@Nullable String limit) {
String query = COVER + " = ?";
String[] args = new String[] { "1" };
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, limit);
setNotifyStickerPackListeners(cursor);
return cursor;
}
public @Nullable Cursor getStickersForPack(@NonNull String packId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String selection = PACK_ID + " = ? AND " + COVER + " = ?";
String[] args = new String[] { packId, "0" };
Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null);
setNotifyStickerListeners(cursor);
return cursor;
}
public @Nullable Cursor getRecentlyUsedStickers(int limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String selection = LAST_USED + " > ? AND " + COVER + " = ?";
String[] args = new String[] { "0", "0" };
Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, LAST_USED + " DESC", String.valueOf(limit));
setNotifyStickerListeners(cursor);
return cursor;
}
public @Nullable InputStream getStickerStream(long rowId) throws IOException {
String selection = _ID + " = ?";
String[] args = new String[] { String.valueOf(rowId) };
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
String path = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(FILE_RANDOM));
if (path != null) {
return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(path), 0);
} else {
Log.w(TAG, "getStickerStream("+rowId+") - No sticker data");
}
} else {
Log.i(TAG, "getStickerStream("+rowId+") - Sticker not found.");
}
}
return null;
}
public boolean isPackInstalled(@NonNull String packId) {
StickerPackRecord record = getStickerPack(packId);
return (record != null && record.isInstalled());
}
public boolean isPackAvailableAsReference(@NonNull String packId) {
return getStickerPack(packId) != null;
}
public void updateStickerLastUsedTime(long rowId, long lastUsed) {
String selection = _ID + " = ?";
String[] args = new String[] { String.valueOf(rowId) };
ContentValues values = new ContentValues();
values.put(LAST_USED, lastUsed);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, selection, args);
notifyStickerListeners();
notifyStickerPackListeners();
}
public void markPackAsInstalled(@NonNull String packKey) {
updatePackInstalled(databaseHelper.getWritableDatabase(), packKey, true);
notifyStickerPackListeners();
}
public void deleteOrphanedPacks() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String query = "SELECT " + PACK_ID + " FROM " + TABLE_NAME + " WHERE " + INSTALLED + " = ? AND " +
PACK_ID + " NOT IN (" +
"SELECT DISTINCT " + AttachmentDatabase.STICKER_PACK_ID + " FROM " + AttachmentDatabase.TABLE_NAME + " " +
"WHERE " + AttachmentDatabase.STICKER_PACK_ID + " NOT NULL" +
")";
String[] args = new String[] { "0" };
db.beginTransaction();
try {
boolean performedDelete = false;
try (Cursor cursor = db.rawQuery(query, args)) {
while (cursor != null && cursor.moveToNext()) {
String packId = cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID));
if (!BlessedPacks.contains(packId)) {
deletePack(db, packId);
performedDelete = true;
}
}
}
db.setTransactionSuccessful();
if (performedDelete) {
notifyStickerPackListeners();
notifyStickerListeners();
}
} finally {
db.endTransaction();
}
}
public void uninstallPack(@NonNull String packId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
updatePackInstalled(db, packId, false);
deleteStickersInPackExceptCover(db, packId);
db.setTransactionSuccessful();
notifyStickerPackListeners();
notifyStickerListeners();
} finally {
db.endTransaction();
}
}
private void updatePackInstalled(@NonNull SQLiteDatabase db, @NonNull String packId, boolean installed) {
StickerPackRecord existing = getStickerPack(packId);
if (existing != null && existing.isInstalled() == installed) {
return;
}
String selection = PACK_ID + " = ?";
String[] args = new String[]{ packId };
ContentValues values = new ContentValues(1);
values.put(INSTALLED, installed ? 1 : 0);
db.update(TABLE_NAME, values, selection, args);
if (installed) {
broadcastInstallEvent(packId);
}
}
private FileInfo saveStickerImage(@NonNull InputStream inputStream) throws IOException {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File file = File.createTempFile("sticker", ".mms", partsDirectory);
Pair<byte[], OutputStream> out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, file, false);
long length = Util.copy(inputStream, out.second);
return new FileInfo(file, length, out.first);
}
private void deleteSticker(@NonNull SQLiteDatabase db, long rowId, @Nullable String filePath) {
String selection = _ID + " = ?";
String[] args = new String[] { String.valueOf(rowId) };
db.delete(TABLE_NAME, selection, args);
if (!TextUtils.isEmpty(filePath)) {
new File(filePath).delete();
}
}
private void deletePack(@NonNull SQLiteDatabase db, @NonNull String packId) {
String selection = PACK_ID + " = ?";
String[] args = new String[] { packId };
db.delete(TABLE_NAME, selection, args);
deleteStickersInPack(db, packId);
}
private void deleteStickersInPack(@NonNull SQLiteDatabase db, @NonNull String packId) {
String selection = PACK_ID + " = ?";
String[] args = new String[] { packId };
db.beginTransaction();
try {
try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
deleteSticker(db, rowId, filePath);
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
db.delete(TABLE_NAME, selection, args);
}
private void deleteStickersInPackExceptCover(@NonNull SQLiteDatabase db, @NonNull String packId) {
String selection = PACK_ID + " = ? AND " + COVER + " = ?";
String[] args = new String[] { packId, "0" };
db.beginTransaction();
try {
try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
deleteSticker(db, rowId, filePath);
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void broadcastInstallEvent(@NonNull String packId) {
StickerPackRecord pack = getStickerPack(packId);
if (pack != null) {
EventBus.getDefault().postSticky(new StickerPackInstallEvent(new DecryptableUri(pack.getCover().getUri())));
}
}
private static final class FileInfo {
private final File file;
private final long length;
private final byte[] random;
private FileInfo(@NonNull File file, long length, @NonNull byte[] random) {
this.file = file;
this.length = length;
this.random = random;
}
public File getFile() {
return file;
}
public long getLength() {
return length;
}
public byte[] getRandom() {
return random;
}
}
public static final class StickerRecordReader implements Closeable {
private final Cursor cursor;
public StickerRecordReader(@Nullable Cursor cursor) {
this.cursor = cursor;
}
public @Nullable StickerRecord getNext() {
if (cursor == null || !cursor.moveToNext()) {
return null;
}
return getCurrent();
}
public @NonNull StickerRecord getCurrent() {
return new StickerRecord(cursor.getLong(cursor.getColumnIndexOrThrow(_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(EMOJI)),
cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LENGTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(COVER)) == 1);
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
}
}
}
public static final class StickerPackRecordReader implements Closeable {
private final Cursor cursor;
public StickerPackRecordReader(@Nullable Cursor cursor) {
this.cursor = cursor;
}
public @Nullable StickerPackRecord getNext() {
if (cursor == null || !cursor.moveToNext()) {
return null;
}
return getCurrent();
}
public @NonNull StickerPackRecord getCurrent() {
StickerRecord cover = new StickerRecordReader(cursor).getCurrent();
return new StickerPackRecord(cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(PACK_TITLE)),
cursor.getString(cursor.getColumnIndexOrThrow(PACK_AUTHOR)),
cover,
cursor.getInt(cursor.getColumnIndexOrThrow(INSTALLED)) == 1);
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
}
}
}
}

View File

@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
@ -97,8 +96,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
for (String sql : JobDatabase.CREATE_TABLE) {
db.execSQL(sql);
}
db.execSQL(StickerDatabase.CREATE_TABLE);
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand());
db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand());
@ -132,7 +129,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, DraftDatabase.CREATE_INDEXS);
executeStatements(db, GroupDatabase.CREATE_INDEXS);
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
executeStatements(db, StickerDatabase.CREATE_INDEXES);
}
@Override
@ -246,7 +242,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV21) {
deleteJobRecords(db,
"ClosedGroupUpdateMessageSendJob",
"NullMessageSendJob");
"NullMessageSendJob",
"StickerDownloadJob",
"StickerPackDownloadJob");
}
db.setTransactionSuccessful();

View File

@ -6,8 +6,6 @@ import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.util.CredentialsProvider;
import org.session.libsignal.service.api.util.SleepTimer;
import org.session.libsignal.service.api.util.UptimeSleepTimer;
import org.session.libsignal.service.api.websocket.ConnectivityListener;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
@ -24,16 +22,11 @@ import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.jobs.StickerDownloadJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewRepository;
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import org.thoughtcrime.securesms.util.RealtimeSleepTimer;
import org.session.libsession.utilities.TextSecurePreferences;
@ -55,10 +48,6 @@ import network.loki.messenger.BuildConfig;
TypingSendJob.class,
AttachmentUploadJob.class,
PushDecryptJob.class,
StickerDownloadJob.class,
StickerPackPreviewRepository.class,
StickerRemoteUriLoader.Factory.class,
StickerPackDownloadJob.class,
LinkPreviewRepository.class})
public class SignalCommunicationModule {
@ -95,12 +84,7 @@ public class SignalCommunicationModule {
@Provides
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
if (this.messageReceiver == null) {
SleepTimer sleepTimer = TextSecurePreferences.isFcmDisabled(context) ? new RealtimeSleepTimer(context) : new UptimeSleepTimer();
this.messageReceiver = new SignalServiceMessageReceiver(new DynamicCredentialsProvider(context),
BuildConfig.USER_AGENT,
new PipeConnectivityListener(),
sleepTimer);
this.messageReceiver = new SignalServiceMessageReceiver();
}
return this.messageReceiver;

View File

@ -45,7 +45,6 @@ public final class JobManagerFactories {
put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory());
put(PushGroupUpdateJob.KEY, new PushGroupUpdateJob.Factory());
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
@ -54,8 +53,6 @@ public final class JobManagerFactories {
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
put(SmsSendJob.KEY, new SmsSendJob.Factory());
put(SmsSentJob.KEY, new SmsSentJob.Factory());
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
put(TypingSendJob.KEY, new TypingSendJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());

View File

@ -1,119 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.IncomingSticker;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class StickerDownloadJob extends BaseJob implements InjectableType {
public static final String KEY = "StickerDownloadJob";
private static final String TAG = Log.tag(StickerDownloadJob.class);
private static final String KEY_PACK_ID = "pack_id";
private static final String KEY_PACK_KEY = "pack_key";
private static final String KEY_PACK_TITLE = "pack_title";
private static final String KEY_PACK_AUTHOR = "pack_author";
private static final String KEY_STICKER_ID = "sticker_id";
private static final String KEY_EMOJI = "emoji";
private static final String KEY_COVER = "cover";
private static final String KEY_INSTALLED = "installed";
private final IncomingSticker sticker;
@Inject SignalServiceMessageReceiver receiver;
public StickerDownloadJob(@NonNull IncomingSticker sticker) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.build(),
sticker);
}
private StickerDownloadJob(@NonNull Job.Parameters parameters, @NonNull IncomingSticker sticker) {
super(parameters);
this.sticker = sticker;
}
@Override
protected void onRun() throws Exception {
StickerDatabase db = DatabaseFactory.getStickerDatabase(context);
if (db.getSticker(sticker.getPackId(), sticker.getStickerId(), sticker.isCover()) != null) {
Log.w(TAG, "Sticker already downloaded.");
return;
}
if (!db.isPackInstalled(sticker.getPackId()) && !sticker.isCover()) {
Log.w(TAG, "Pack is no longer installed.");
return;
}
byte[] packIdBytes = Hex.fromStringCondensed(sticker.getPackId());
byte[] packKeyBytes = Hex.fromStringCondensed(sticker.getPackKey());
InputStream stream = receiver.retrieveSticker(packIdBytes, packKeyBytes, sticker.getStickerId());
db.insertSticker(sticker, stream);
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putString(KEY_PACK_ID, sticker.getPackId())
.putString(KEY_PACK_KEY, sticker.getPackKey())
.putString(KEY_PACK_TITLE, sticker.getPackTitle())
.putString(KEY_PACK_AUTHOR, sticker.getPackAuthor())
.putInt(KEY_STICKER_ID, sticker.getStickerId())
.putString(KEY_EMOJI, sticker.getEmoji())
.putBoolean(KEY_COVER, sticker.isCover())
.putBoolean(KEY_INSTALLED, sticker.isInstalled())
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onCanceled() {
Log.w(TAG, "Failed to download sticker!");
}
public static final class Factory implements Job.Factory<StickerDownloadJob> {
@Override
public @NonNull StickerDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
IncomingSticker sticker = new IncomingSticker(data.getString(KEY_PACK_ID),
data.getString(KEY_PACK_KEY),
data.getString(KEY_PACK_TITLE),
data.getString(KEY_PACK_AUTHOR),
data.getInt(KEY_STICKER_ID),
data.getString(KEY_EMOJI),
data.getBoolean(KEY_COVER),
data.getBoolean(KEY_INSTALLED));
return new StickerDownloadJob(parameters, sticker);
}
}
}

View File

@ -1,161 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.IncomingSticker;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.messages.SignalServiceStickerManifest;
import org.session.libsignal.service.api.messages.SignalServiceStickerManifest.StickerInfo;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class StickerPackDownloadJob extends BaseJob implements InjectableType {
public static final String KEY = "StickerPackDownloadJob";
private static final String TAG = Log.tag(StickerPackDownloadJob.class);
private static final String KEY_PACK_ID = "pack_key";
private static final String KEY_PACK_KEY = "pack_id";
private static final String KEY_REFERENCE_PACK = "reference_pack";
private final String packId;
private final String packKey;
private final boolean isReferencePack;
@Inject SignalServiceMessageReceiver receiver;
public StickerPackDownloadJob(@NonNull String packId, @NonNull String packKey, boolean isReferencePack)
{
this(new Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setQueue("StickerPackDownloadJob_" + packKey)
.build(),
packId,
packKey,
isReferencePack);
}
private StickerPackDownloadJob(@NonNull Parameters parameters,
@NonNull String packId,
@NonNull String packKey,
boolean isReferencePack)
{
super(parameters);
this.packId = packId;
this.packKey = packKey;
this.isReferencePack = isReferencePack;
}
@Override
protected void onRun() throws IOException, InvalidMessageException {
if (isReferencePack && !DatabaseFactory.getAttachmentDatabase(context).containsStickerPackId(packId)) {
Log.w(TAG, "There are no attachments with the requested packId present for this reference pack. Skipping.");
return;
}
if (isReferencePack && DatabaseFactory.getStickerDatabase(context).isPackAvailableAsReference(packId)) {
Log.i(TAG, "Sticker pack already available for reference. Skipping.");
return;
}
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
byte[] packIdBytes = Hex.fromStringCondensed(packId);
byte[] packKeyBytes = Hex.fromStringCondensed(packKey);
SignalServiceStickerManifest manifest = receiver.retrieveStickerManifest(packIdBytes, packKeyBytes);
if (manifest.getStickers().isEmpty()) {
Log.w(TAG, "No stickers in pack!");
return;
}
if (!isReferencePack && stickerDatabase.isPackAvailableAsReference(packId)) {
stickerDatabase.markPackAsInstalled(packId);
}
StickerInfo cover = manifest.getCover().or(manifest.getStickers().get(0));
JobManager.Chain chain = jobManager.startChain(new StickerDownloadJob(new IncomingSticker(packId,
packKey,
manifest.getTitle().or(""),
manifest.getAuthor().or(""),
cover.getId(),
"",
true,
!isReferencePack)));
if (!isReferencePack) {
List<Job> jobs = new ArrayList<>(manifest.getStickers().size());
for (StickerInfo stickerInfo : manifest.getStickers()) {
jobs.add(new StickerDownloadJob(new IncomingSticker(packId,
packKey,
manifest.getTitle().or(""),
manifest.getAuthor().or(""),
stickerInfo.getId(),
stickerInfo.getEmoji(),
false,
true)));
}
chain.then(jobs);
}
chain.enqueue();
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putString(KEY_PACK_ID, packId)
.putString(KEY_PACK_KEY, packKey)
.putBoolean(KEY_REFERENCE_PACK, isReferencePack)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onCanceled() {
Log.w(TAG, "Failed to download manifest with pack_id: " + packId);
}
public static final class Factory implements Job.Factory<StickerPackDownloadJob> {
@Override
public @NonNull
StickerPackDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new StickerPackDownloadJob(parameters,
data.getString(KEY_PACK_ID),
data.getString(KEY_PACK_KEY),
data.getBoolean(KEY_REFERENCE_PACK));
}
}
}

View File

@ -7,30 +7,20 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.gms.common.util.IOUtils;
import org.session.libsession.utilities.MediaTypes;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.net.CallRequestController;
import org.thoughtcrime.securesms.net.CompositeRequestController;
import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
import org.thoughtcrime.securesms.net.RequestController;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.Pair;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.messages.SignalServiceStickerManifest;
import org.session.libsignal.service.api.messages.SignalServiceStickerManifest.StickerInfo;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
@ -41,7 +31,6 @@ import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
@ -82,31 +71,27 @@ public class LinkPreviewRepository implements InjectableType {
RequestController metadataController;
if (StickerUrl.isValidShareLink(url)) {
metadataController = fetchStickerPackLinkPreview(context, url, callback);
} else {
metadataController = fetchMetadata(url, metadata -> {
if (metadata.isEmpty()) {
metadataController = fetchMetadata(url, metadata -> {
if (metadata.isEmpty()) {
callback.onComplete(Optional.absent());
return;
}
if (!metadata.getImageUrl().isPresent()) {
callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get(), Optional.absent())));
return;
}
RequestController imageController = fetchThumbnail(context, metadata.getImageUrl().get(), attachment -> {
if (!metadata.getTitle().isPresent() && !attachment.isPresent()) {
callback.onComplete(Optional.absent());
return;
} else {
callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().or(""), attachment)));
}
if (!metadata.getImageUrl().isPresent()) {
callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get(), Optional.absent())));
return;
}
RequestController imageController = fetchThumbnail(context, metadata.getImageUrl().get(), attachment -> {
if (!metadata.getTitle().isPresent() && !attachment.isPresent()) {
callback.onComplete(Optional.absent());
} else {
callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().or(""), attachment)));
}
});
compositeController.addController(imageController);
});
}
compositeController.addController(imageController);
});
compositeController.addController(metadataController);
return compositeController;
@ -190,66 +175,6 @@ public class LinkPreviewRepository implements InjectableType {
return controller;
}
private RequestController fetchStickerPackLinkPreview(@NonNull Context context,
@NonNull String packUrl,
@NonNull Callback<Optional<LinkPreview>> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
try {
Pair<String, String> stickerParams = StickerUrl.parseShareLink(packUrl).or(new Pair<>("", ""));
String packIdString = stickerParams.first();
String packKeyString = stickerParams.second();
byte[] packIdBytes = Hex.fromStringCondensed(packIdString);
byte[] packKeyBytes = Hex.fromStringCondensed(packKeyString);
SignalServiceStickerManifest manifest = messageReceiver.retrieveStickerManifest(packIdBytes, packKeyBytes);
String title = manifest.getTitle().or(manifest.getAuthor()).or("");
Optional<StickerInfo> firstSticker = Optional.fromNullable(manifest.getStickers().size() > 0 ? manifest.getStickers().get(0) : null);
Optional<StickerInfo> cover = manifest.getCover().or(firstSticker);
if (cover.isPresent()) {
Bitmap bitmap = GlideApp.with(context).asBitmap()
.load(new StickerRemoteUri(packIdString, packKeyString, cover.get().getId()))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerInside()
.submit(512, 512)
.get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.WEBP, 80, baos);
byte[] bytes = baos.toByteArray();
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri,
uri,
MediaTypes.IMAGE_WEBP,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
bytes.length,
bitmap.getWidth(),
bitmap.getHeight(),
null,
null,
false,
false,
null,
null));
callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail)));
} else {
callback.onComplete(Optional.absent());
}
} catch (IOException | InvalidMessageException | ExecutionException | InterruptedException e) {
Log.w(TAG, "Failed to fetch sticker pack link preview.");
callback.onComplete(Optional.absent());
}
});
return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect.");
}
private static Optional<Attachment> bitmapToAttachment(@Nullable Bitmap bitmap,
@NonNull Bitmap.CompressFormat format,
@NonNull String contentType)

View File

@ -12,7 +12,6 @@ import android.text.util.Linkify;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsignal.libsignal.util.guava.Optional;
@ -62,7 +61,6 @@ public final class LinkPreviewUtil {
*/
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
if (linkUrl == null) return false;
if (StickerUrl.isValidShareLink(linkUrl)) return true;
HttpUrl url = HttpUrl.parse(linkUrl);
return url != null &&

View File

@ -52,7 +52,6 @@ public class PartAuthority {
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);
default: return context.getContentResolver().openInputStream(uri);

View File

@ -34,8 +34,6 @@ import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import java.io.File;
import java.io.InputStream;
@ -71,7 +69,6 @@ public class SignalGlideModule extends AppGlideModule {
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory(context));
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}

View File

@ -4,13 +4,9 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ApplicationContext.getInstance(context).getJobManager().add(new PushNotificationReceiveJob(context));
}
}

View File

@ -1,18 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import java.util.HashSet;
import java.util.Set;
/**
* Maintains a list of "blessed" sticker packs that essentially serve as defaults.
*/
public final class BlessedPacks {
private static final Set<String> BLESSED_PACK_IDS = new HashSet<>();
public static boolean contains(@NonNull String packId) {
return BLESSED_PACK_IDS.contains(packId);
}
}

View File

@ -1,135 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.ArrayList;
import java.util.List;
import network.loki.messenger.R;
/**
* Adapter for a specific page in the sticker keyboard. Shows the stickers in a grid.
* @see StickerKeyboardPageFragment
*/
final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder> {
private final GlideRequests glideRequests;
private final EventListener eventListener;
private final List<StickerRecord> stickers;
private int stickerSize;
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
this.glideRequests = glideRequests;
this.eventListener = eventListener;
this.stickers = new ArrayList<>();
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
return stickers.get(position).getRowId();
}
@Override
public @NonNull
StickerKeyboardPageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new StickerKeyboardPageViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_keyboard_page_list_item, viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull StickerKeyboardPageViewHolder viewHolder, int i) {
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize);
}
@Override
public void onViewRecycled(@NonNull StickerKeyboardPageViewHolder holder) {
holder.recycle();
}
@Override
public int getItemCount() {
return stickers.size();
}
void setStickers(@NonNull List<StickerRecord> stickers, @Px int stickerSize) {
this.stickers.clear();
this.stickers.addAll(stickers);
this.stickerSize = stickerSize;
notifyDataSetChanged();
}
void setStickerSize(@Px int stickerSize) {
this.stickerSize = stickerSize;
notifyDataSetChanged();
}
static class StickerKeyboardPageViewHolder extends RecyclerView.ViewHolder {
private final ImageView image;
private StickerRecord currentSticker;
public StickerKeyboardPageViewHolder(@NonNull View itemView) {
super(itemView);
image = itemView.findViewById(R.id.sticker_keyboard_page_image);
}
public void bind(@NonNull GlideRequests glideRequests,
@Nullable EventListener eventListener,
@NonNull StickerRecord sticker,
@Px int size)
{
currentSticker = sticker;
itemView.getLayoutParams().height = size;
itemView.getLayoutParams().width = size;
itemView.requestLayout();
glideRequests.load(new DecryptableUri(sticker.getUri()))
.transition(DrawableTransitionOptions.withCrossFade())
.into(image);
if (eventListener != null) {
image.setOnClickListener(v -> eventListener.onStickerClicked(sticker));
image.setOnLongClickListener(v -> {
eventListener.onStickerLongClicked(v);
return true;
});
} else {
image.setOnClickListener(null);
image.setOnLongClickListener(null);
}
}
void recycle() {
image.setOnClickListener(null);
}
@Nullable StickerRecord getCurrentSticker() {
return currentSticker;
}
}
interface EventListener {
void onStickerClicked(@NonNull StickerRecord sticker);
void onStickerLongClicked(@NonNull View targetView);
}
}

View File

@ -1,217 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder;
import org.session.libsession.utilities.ViewUtil;
import network.loki.messenger.R;
/**
* An individual page of stickers in the {@link StickerKeyboardProvider}.
*/
public final class StickerKeyboardPageFragment extends Fragment implements StickerKeyboardPageAdapter.EventListener {
private static final String TAG = Log.tag(StickerKeyboardPageFragment.class);
private static final String KEY_PACK_ID = "pack_id";
public static final String RECENT_PACK_ID = StickerKeyboardPageViewModel.RECENT_PACK_ID;
private RecyclerView list;
private StickerKeyboardPageAdapter adapter;
private GridLayoutManager layoutManager;
private StickerKeyboardPageViewModel viewModel;
private EventListener eventListener;
private ListTouchListener listTouchListener;
private String packId;
public static StickerKeyboardPageFragment newInstance(@NonNull String packId) {
Bundle args = new Bundle();
args.putString(KEY_PACK_ID, packId);
StickerKeyboardPageFragment fragment = new StickerKeyboardPageFragment();
fragment.setArguments(args);
fragment.packId = packId;
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.sticker_keyboard_page, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
GlideRequests glideRequests = GlideApp.with(this);
this.list = view.findViewById(R.id.sticker_keyboard_list);
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this);
this.layoutManager = new GridLayoutManager(requireContext(), 2);
this.listTouchListener = new ListTouchListener(requireContext(), glideRequests);
this.packId = getArguments().getString(KEY_PACK_ID);
list.setLayoutManager(layoutManager);
list.setAdapter(adapter);
list.addOnItemTouchListener(listTouchListener);
initViewModel(packId);
onScreenWidthChanged(getScreenWidth());
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
onScreenWidthChanged(getScreenWidth());
}
@Override
public void onStickerClicked(@NonNull StickerRecord sticker) {
if (eventListener != null) {
eventListener.onStickerSelected(sticker);
}
}
@Override
public void onStickerLongClicked(@NonNull View targetView) {
if (listTouchListener != null) {
listTouchListener.enterHoverMode(list, targetView);
}
}
public void setEventListener(@NonNull EventListener eventListener) {
this.eventListener = eventListener;
}
public @NonNull String getPackId() {
return packId;
}
private void initViewModel(@NonNull String packId) {
StickerKeyboardRepository repository = new StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(requireContext()));
viewModel = ViewModelProviders.of(this, new StickerKeyboardPageViewModel.Factory(requireActivity().getApplication(), repository)).get(StickerKeyboardPageViewModel.class);
viewModel.getStickers(packId).observe(getViewLifecycleOwner(), stickerRecords -> {
if (stickerRecords == null) return;
adapter.setStickers(stickerRecords, calculateStickerSize(getScreenWidth()));
});
}
private void onScreenWidthChanged(@Px int newWidth) {
if (layoutManager != null) {
layoutManager.setSpanCount(calculateColumnCount(newWidth));
adapter.setStickerSize(calculateStickerSize(newWidth));
}
}
private int getScreenWidth() {
Point size = new Point();
requireActivity().getWindowManager().getDefaultDisplay().getSize(size);
return size.x;
}
private int calculateColumnCount(@Px int screenWidth) {
float modifier = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_padding);
float divisor = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_divisor);
return (int) ((screenWidth - modifier) / divisor);
}
private int calculateStickerSize(@Px int screenWidth) {
float multiplier = getResources().getDimensionPixelOffset(R.dimen.sticker_page_item_multiplier);
int columnCount = calculateColumnCount(screenWidth);
return (int) ((screenWidth - ((columnCount + 1) * multiplier)) / columnCount);
}
private final class ListTouchListener implements RecyclerView.OnItemTouchListener {
private final StickerPreviewPopup popup;
private boolean hoverMode;
ListTouchListener(@NonNull Context context, @NonNull GlideRequests glideRequests) {
this.popup = new StickerPreviewPopup(context, glideRequests);
popup.setAnimationStyle(R.style.StickerPopupAnimation);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
return hoverMode;
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
hoverMode = false;
popup.dismiss();
eventListener.onStickerPopupEnded();
break;
default:
for (int i = 0, len = recyclerView.getChildCount(); i < len; i++) {
View child = recyclerView.getChildAt(i);
if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) &&
ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()))
{
showStickerForView(recyclerView, child);
}
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean b) {
}
void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
this.hoverMode = true;
showStickerForView(recyclerView, targetView);
}
private void showStickerForView(@NonNull RecyclerView recyclerView, @NonNull View view) {
StickerKeyboardPageViewHolder holder = (StickerKeyboardPageViewHolder) recyclerView.getChildViewHolder(view);
if (holder != null && holder.getCurrentSticker() != null) {
if (!popup.isShowing()) {
popup.showAtLocation(recyclerView, Gravity.NO_GRAVITY, 0, 0);
eventListener.onStickerPopupStarted();
}
popup.presentSticker(holder.getCurrentSticker());
}
}
}
interface EventListener {
void onStickerSelected(@NonNull StickerRecord sticker);
void onStickerPopupStarted();
void onStickerPopupEnded();
}
}

View File

@ -1,77 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.session.libsession.utilities.Throttler;
import java.util.List;
final class StickerKeyboardPageViewModel extends ViewModel {
static final String RECENT_PACK_ID = "RECENT";
private final Application application;
private final StickerKeyboardRepository repository;
private final MutableLiveData<List<StickerRecord>> stickers;
private final Throttler observerThrottler;
private final ContentObserver observer;
private String packId;
private StickerKeyboardPageViewModel(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
this.application = application;
this.repository = repository;
this.stickers = new MutableLiveData<>();
this.observerThrottler = new Throttler(500);
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
observerThrottler.publish(() -> getStickers(packId));
}
};
application.getContentResolver().registerContentObserver(DatabaseContentProviders.Sticker.CONTENT_URI, true, observer);
}
LiveData<List<StickerRecord>> getStickers(@NonNull String packId) {
this.packId = packId;
if (RECENT_PACK_ID.equals(packId)) {
repository.getRecentStickers(stickers::postValue);
} else {
repository.getStickersForPack(packId, stickers::postValue);
}
return stickers;
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(observer);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerKeyboardRepository repository;
Factory(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new StickerKeyboardPageViewModel(application, repository));
}
}
}

View File

@ -1,259 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.ImageView;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageFragment.EventListener;
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
import org.thoughtcrime.securesms.util.ResUtil;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.Throttler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import network.loki.messenger.R;
/**
* A provider to select stickers in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
*/
public final class StickerKeyboardProvider implements MediaKeyboardProvider,
MediaKeyboardProvider.AddObserver,
StickerKeyboardPageFragment.EventListener
{
private final Context context;
private final StickerEventListener eventListener;
private final StickerPagerAdapter pagerAdapter;
private final Throttler stickerThrottler;
private Controller controller;
private Presenter presenter;
private boolean isSoloProvider;
private StickerKeyboardViewModel viewModel;
public StickerKeyboardProvider(@NonNull AppCompatActivity activity,
@NonNull StickerEventListener eventListener)
{
this.context = activity;
this.eventListener = eventListener;
this.pagerAdapter = new StickerPagerAdapter(activity.getSupportFragmentManager(), this);
this.stickerThrottler = new Throttler(100);
initViewModel(activity);
}
@Override
public int getProviderIconView(boolean selected) {
if (selected) {
return ThemeUtil.isDarkTheme(context) ? R.layout.sticker_keyboard_icon_dark_selected : R.layout.sticker_keyboard_icon_light_selected;
} else {
return ThemeUtil.isDarkTheme(context) ? R.layout.sticker_keyboard_icon_dark : R.layout.sticker_keyboard_icon_light;
}
}
@Override
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
this.presenter = presenter;
this.isSoloProvider = isSoloProvider;
PackListResult result = viewModel.getPacks().getValue();
if (result != null) {
present(presenter, result, true);
}
}
@Override
public void setController(@Nullable Controller controller) {
this.controller = controller;
}
@Override
public void onAddClicked() {
eventListener.onStickerManagementClicked();
}
@Override
public void onStickerSelected(@NonNull StickerRecord sticker) {
stickerThrottler.publish(() -> eventListener.onStickerSelected(sticker));
}
@Override
public void onStickerPopupStarted() {
if (controller != null) {
controller.setViewPagerEnabled(false);
}
}
@Override
public void onStickerPopupEnded() {
if (controller != null) {
controller.setViewPagerEnabled(true);
}
}
private void initViewModel(@NonNull AppCompatActivity activity) {
StickerKeyboardRepository repository = new StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(activity));
viewModel = ViewModelProviders.of(activity, new StickerKeyboardViewModel.Factory(activity.getApplication(), repository)).get(StickerKeyboardViewModel.class);
viewModel.getPacks().observe(activity, result -> {
if (result == null) return;
int previousCount = pagerAdapter.getCount();
pagerAdapter.setPacks(result.getPacks());
if (presenter != null) {
present(presenter, result, previousCount != pagerAdapter.getCount());
}
});
}
private void present(@NonNull Presenter presenter, @NonNull PackListResult result, boolean calculateStartingIndex) {
if (result.getPacks().isEmpty() && presenter.isVisible()) {
context.startActivity(StickerManagementActivity.getIntent(context));
presenter.requestDismissal();
return;
}
int startingIndex = presenter.getCurrentPosition();
if (calculateStartingIndex) {
startingIndex = !result.hasRecents() && result.getPacks().size() > 0 ? 1 : 0;
}
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks()), null, this, null, startingIndex);
if (isSoloProvider && result.getPacks().isEmpty()) {
context.startActivity(StickerManagementActivity.getIntent(context));
}
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof StickerKeyboardProvider;
}
private static class StickerPagerAdapter extends FragmentStatePagerAdapter {
private final List<StickerPackRecord> packs;
private final Map<String, Integer> itemPositions;
private final EventListener eventListener;
public StickerPagerAdapter(@NonNull FragmentManager fm, @NonNull EventListener eventListener) {
super(fm);
this.eventListener = eventListener;
this.packs = new ArrayList<>();
this.itemPositions = new HashMap<>();
}
@Override
public int getItemPosition(@NonNull Object object) {
String packId = ((StickerKeyboardPageFragment) object).getPackId();
if (itemPositions.containsKey(packId)) {
//noinspection ConstantConditions
return itemPositions.get(packId);
}
return POSITION_NONE;
}
@Override
public Fragment getItem(int i) {
StickerKeyboardPageFragment fragment;
if (i == 0) {
fragment = StickerKeyboardPageFragment.newInstance(StickerKeyboardPageFragment.RECENT_PACK_ID);
} else {
StickerPackRecord pack = packs.get(i - 1);
fragment = StickerKeyboardPageFragment.newInstance(pack.getPackId());
}
fragment.setEventListener(eventListener);
return fragment;
}
@Override
public int getCount() {
return packs.isEmpty() ? 0 : packs.size() + 1;
}
void setPacks(@NonNull List<StickerPackRecord> packs) {
itemPositions.clear();
if (areListsEqual(this.packs, packs)) {
itemPositions.put(StickerKeyboardPageFragment.RECENT_PACK_ID, 0);
for (int i = 0; i < packs.size(); i++) {
itemPositions.put(packs.get(i).getPackId(), i + 1);
}
}
this.packs.clear();
this.packs.addAll(packs);
notifyDataSetChanged();
}
boolean areListsEqual(@NonNull List<StickerPackRecord> a, @NonNull List<StickerPackRecord> b) {
if (a.size() != b.size()) return false;
for (int i = 0; i < a.size(); i++) {
if (!a.get(i).equals(b.get(i))) {
return false;
}
}
return true;
}
}
private static class IconProvider implements TabIconProvider {
private final Context context;
private final List<StickerPackRecord> packs;
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs) {
this.context = context;
this.packs = packs;
}
@Override
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
if (index == 0) {
Drawable icon = ResUtil.getDrawable(context, R.attr.emoji_category_recent);
imageView.setImageDrawable(icon);
} else {
Uri uri = packs.get(index - 1).getCover().getUri();
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
.into(imageView);
}
}
}
public interface StickerEventListener {
void onStickerSelected(@NonNull StickerRecord sticker);
void onStickerManagementClicked();
}
}

View File

@ -1,99 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase.StickerPackRecordReader;
import org.thoughtcrime.securesms.database.StickerDatabase.StickerRecordReader;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.List;
final class StickerKeyboardRepository {
private static final int RECENT_LIMIT = 24;
private final StickerDatabase stickerDatabase;
StickerKeyboardRepository(@NonNull StickerDatabase stickerDatabase) {
this.stickerDatabase = stickerDatabase;
}
void getPackList(@NonNull Callback<PackListResult> callback) {
SignalExecutors.BOUNDED.execute(() -> {
List<StickerPackRecord> packs = new ArrayList<>();
try (StickerPackRecordReader reader = new StickerPackRecordReader(stickerDatabase.getInstalledStickerPacks())) {
StickerPackRecord pack;
while ((pack = reader.getNext()) != null) {
packs.add(pack);
}
}
boolean hasRecents;
try (Cursor recentsCursor = stickerDatabase.getRecentlyUsedStickers(1)) {
hasRecents = recentsCursor != null && recentsCursor.moveToFirst();
}
callback.onComplete(new PackListResult(packs, hasRecents));
});
}
void getStickersForPack(@NonNull String packId, @NonNull Callback<List<StickerRecord>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
List<StickerRecord> stickers = new ArrayList<>();
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersForPack(packId))) {
StickerRecord sticker;
while ((sticker = reader.getNext()) != null) {
stickers.add(sticker);
}
}
callback.onComplete(stickers);
});
}
void getRecentStickers(@NonNull Callback<List<StickerRecord>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
List<StickerRecord> stickers = new ArrayList<>();
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT))) {
StickerRecord sticker;
while ((sticker = reader.getNext()) != null) {
stickers.add(sticker);
}
}
callback.onComplete(stickers);
});
}
static class PackListResult {
private final List<StickerPackRecord> packs;
private final boolean hasRecents;
PackListResult(List<StickerPackRecord> packs, boolean hasRecents) {
this.packs = packs;
this.hasRecents = hasRecents;
}
List<StickerPackRecord> getPacks() {
return packs;
}
boolean hasRecents() {
return hasRecents;
}
}
interface Callback<T> {
void onComplete(T result);
}
}

View File

@ -1,62 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
import org.session.libsession.utilities.Throttler;
final class StickerKeyboardViewModel extends ViewModel {
private final Application application;
private final MutableLiveData<PackListResult> packs;
private final Throttler observerThrottler;
private final ContentObserver observer;
private StickerKeyboardViewModel(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
this.application = application;
this.packs = new MutableLiveData<>();
this.observerThrottler = new Throttler(500);
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
observerThrottler.publish(() -> repository.getPackList(packs::postValue));
}
};
repository.getPackList(packs::postValue);
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, observer);
}
@NonNull LiveData<PackListResult> getPacks() {
return packs;
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(observer);
}
public static final class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerKeyboardRepository repository;
public Factory(@NonNull Application application, @NonNull StickerKeyboardRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new StickerKeyboardViewModel(application, repository));
}
}
}

View File

@ -1,113 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.MenuItem;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import network.loki.messenger.R;
/**
* Allows the user to view and manage (install, uninstall, etc) their stickers.
*/
public final class StickerManagementActivity extends PassphraseRequiredActionBarActivity implements StickerManagementAdapter.EventListener {
private RecyclerView list;
private StickerManagementAdapter adapter;
private StickerManagementViewModel viewModel;
public static Intent getIntent(@NonNull Context context) {
return new Intent(context, StickerManagementActivity.class);
}
@Override
protected void onPreCreate() {
super.onPreCreate();
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
setContentView(R.layout.sticker_management_activity);
initView();
initToolbar();
initViewModel();
}
@Override
protected void onStart() {
super.onStart();
viewModel.onVisible();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onStickerPackClicked(@NonNull String packId, @NonNull String packKey) {
startActivity(StickerPackPreviewActivity.getIntent(packId, packKey));
}
@Override
public void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey) {
viewModel.onStickerPackUninstallClicked(packId, packKey);
}
@Override
public void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey) {
viewModel.onStickerPackInstallClicked(packId, packKey);
}
@Override
public void onStickerPackShareClicked(@NonNull String packId, @NonNull String packKey) {
Intent composeIntent = new Intent(this, ShareActivity.class);
composeIntent.putExtra(Intent.EXTRA_TEXT, StickerUrl.createShareLink(packId, packKey));
startActivity(composeIntent);
finish();
}
private void initView() {
this.list = findViewById(R.id.sticker_management_list);
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
}
private void initToolbar() {
getSupportActionBar().setTitle(R.string.StickerManagementActivity_stickers);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void initViewModel() {
StickerManagementRepository repository = new StickerManagementRepository(this);
viewModel = ViewModelProviders.of(this, new StickerManagementViewModel.Factory(getApplication(), repository)).get(StickerManagementViewModel.class);
viewModel.init();
viewModel.getStickerPacks().observe(this, packResult -> {
if (packResult == null) return;
adapter.setPackLists(packResult.getInstalledPacks(), packResult.getAvailablePacks(), packResult.getBlessedPacks());
});
}
}

View File

@ -1,330 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.StableIdGenerator;
import java.util.ArrayList;
import java.util.List;
import network.loki.messenger.R;
final class StickerManagementAdapter extends RecyclerView.Adapter {
private static final int TYPE_HEADER = 1;
private static final int TYPE_EMPTY = 2;
private static final int TYPE_PACK = 3;
private static final String TAG_YOUR_STICKERS = "YourStickers";
private static final String TAG_MESSAGE_STICKERS = "MessageStickers";
private static final String TAG_BLESSED_STICKERS = "BlessedStickers";
private final GlideRequests glideRequests;
private final EventListener eventListener;
private final StableIdGenerator<String> stableIdGenerator;
private final List<Section> sections = new ArrayList<Section>(3) {{
Section yourStickers = new Section(TAG_YOUR_STICKERS,
R.string.StickerManagementAdapter_installed_stickers,
R.string.StickerManagementAdapter_no_stickers_installed,
new ArrayList<>(),
0);
Section messageStickers = new Section(TAG_MESSAGE_STICKERS,
R.string.StickerManagementAdapter_stickers_you_received,
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
new ArrayList<>(),
yourStickers.size());
add(yourStickers);
add(messageStickers);
}};
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
this.glideRequests = glideRequests;
this.eventListener = eventListener;
this.stableIdGenerator = new StableIdGenerator<>();
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
for (Section section : sections) {
if (section.handles(position)) {
return section.getItemId(stableIdGenerator, position);
}
}
throw new NoSectionException();
}
@Override
public int getItemViewType(int position) {
for (Section section : sections) {
if (section.handles(position)) {
return section.getViewType(position);
}
}
throw new NoSectionException();
}
@Override
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
switch (viewType) {
case TYPE_HEADER:
return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_header_item, viewGroup, false));
case TYPE_EMPTY:
return new EmptyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_empty_item, viewGroup, false));
case TYPE_PACK:
return new StickerViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_management_sticker_item, viewGroup, false));
default:
throw new AssertionError("Unexpected viewType! " + viewType);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
for (Section section : sections) {
if (section.handles(position)) {
section.bindViewHolder(viewHolder, position, glideRequests, eventListener);
return;
}
}
throw new NoSectionException();
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof StickerViewHolder) {
((StickerViewHolder) holder).recycle();
}
}
@Override
public int getItemCount() {
return Stream.of(sections).reduce(0, (sum, section) -> sum + section.size());
}
void setPackLists(@NonNull List<StickerPackRecord> installedPacks,
@NonNull List<StickerPackRecord> availablePacks,
@NonNull List<StickerPackRecord> blessedPacks)
{
Section yourStickers = new Section(TAG_YOUR_STICKERS,
R.string.StickerManagementAdapter_installed_stickers,
R.string.StickerManagementAdapter_no_stickers_installed,
installedPacks,
0);
Section blessedStickers = new Section(TAG_BLESSED_STICKERS,
R.string.StickerManagementAdapter_signal_artist_series,
0,
blessedPacks,
yourStickers.size());
Section messageStickers = new Section(TAG_MESSAGE_STICKERS,
R.string.StickerManagementAdapter_stickers_you_received,
R.string.StickerManagementAdapter_stickers_from_incoming_messages_will_appear_here,
availablePacks,
yourStickers.size() + (blessedPacks.isEmpty() ? 0 : blessedStickers.size()));
sections.clear();
sections.add(yourStickers);
if (!blessedPacks.isEmpty()) {
sections.add(blessedStickers);
}
sections.add(messageStickers);
notifyDataSetChanged();
}
private static class Section {
private static final String STABLE_ID_HEADER = "header";
private static final String STABLE_ID_TEXT = "text";
private final String tag;
private final int titleResId;
private final int emptyResId;
private final List<StickerPackRecord> records;
private final int offset;
Section(@NonNull String tag,
@StringRes int titleResId,
@StringRes int emptyResId,
@NonNull List<StickerPackRecord> records,
int offset)
{
this.tag = tag;
this.titleResId = titleResId;
this.emptyResId = emptyResId;
this.records = records;
this.offset = offset;
}
int getViewType(int globalPosition) {
int localPosition = globalPosition - offset;
if (localPosition == 0) {
return TYPE_HEADER;
} else if (records.isEmpty()) {
return TYPE_EMPTY;
} else {
return TYPE_PACK;
}
}
long getItemId(@NonNull StableIdGenerator<String> idGenerator, int globalPosition) {
int localPosition = globalPosition - offset;
if (localPosition == 0) {
return idGenerator.getId(tag + "_" + STABLE_ID_HEADER);
} else if (records.isEmpty()) {
return idGenerator.getId(tag + "_" + STABLE_ID_TEXT);
} else {
return idGenerator.getId(records.get(localPosition - 1).getPackId());
}
}
void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
int globalPosition,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener)
{
int localPosition = globalPosition - offset;
if (localPosition == 0) {
((HeaderViewHolder) viewHolder).bind(titleResId);
} else if (records.isEmpty()) {
((EmptyViewHolder) viewHolder).bind(emptyResId);
} else {
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size());
}
}
boolean handles(int globalPosition) {
int localPosition = globalPosition - offset;
return localPosition >= 0 && localPosition < size();
}
int size() {
return records.isEmpty() ? 2 : records.size() + 1;
}
}
static class StickerViewHolder extends RecyclerView.ViewHolder {
private final ImageView cover;
private final TextView title;
private final TextView author;
private final View badge;
private final View divider;
private final View actionButton;
private final ImageView actionButtonImage;
private final View shareButton;
private final ImageView shareButtonImage;
StickerViewHolder(@NonNull View itemView) {
super(itemView);
this.cover = itemView.findViewById(R.id.sticker_management_cover);
this.title = itemView.findViewById(R.id.sticker_management_title);
this.author = itemView.findViewById(R.id.sticker_management_author);
this.badge = itemView.findViewById(R.id.sticker_management_blessed_badge);
this.divider = itemView.findViewById(R.id.sticker_management_divider);
this.actionButton = itemView.findViewById(R.id.sticker_management_action_button);
this.actionButtonImage = itemView.findViewById(R.id.sticker_management_action_button_image);
this.shareButton = itemView.findViewById(R.id.sticker_management_share_button);
this.shareButtonImage = itemView.findViewById(R.id.sticker_management_share_button_image);
}
void bind(@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull StickerPackRecord stickerPack,
boolean lastInList)
{
title.setText(stickerPack.getTitle().or(itemView.getResources().getString(R.string.StickerManagementAdapter_untitled)));
author.setText(stickerPack.getAuthor().or(itemView.getResources().getString(R.string.StickerManagementAdapter_unknown)));
divider.setVisibility(lastInList ? View.GONE : View.VISIBLE);
badge.setVisibility(BlessedPacks.contains(stickerPack.getPackId()) ? View.VISIBLE : View.GONE);
glideRequests.load(new DecryptableUri(stickerPack.getCover().getUri()))
.transition(DrawableTransitionOptions.withCrossFade())
.into(cover);
if (stickerPack.isInstalled()) {
actionButtonImage.setImageResource(R.drawable.ic_x);
actionButton.setOnClickListener(v -> eventListener.onStickerPackUninstallClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
shareButton.setVisibility(View.VISIBLE);
shareButtonImage.setVisibility(View.VISIBLE);
shareButton.setOnClickListener(v -> eventListener.onStickerPackShareClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
} else {
actionButtonImage.setImageResource(R.drawable.ic_arrow_down);
actionButton.setOnClickListener(v -> eventListener.onStickerPackInstallClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
shareButton.setVisibility(View.GONE);
shareButtonImage.setVisibility(View.GONE);
shareButton.setOnClickListener(null);
}
itemView.setOnClickListener(v -> eventListener.onStickerPackClicked(stickerPack.getPackId(), stickerPack.getPackKey()));
}
void recycle() {
actionButton.setOnClickListener(null);
shareButton.setOnClickListener(null);
itemView.setOnClickListener(null);
}
}
static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final TextView titleView;
HeaderViewHolder(@NonNull View itemView) {
super(itemView);
this.titleView = itemView.findViewById(R.id.sticker_management_header);
}
void bind(@StringRes int title) {
titleView.setText(title);
}
}
static class EmptyViewHolder extends RecyclerView.ViewHolder {
private final TextView text;
EmptyViewHolder(@NonNull View itemView) {
super(itemView);
this.text = itemView.findViewById(R.id.sticker_management_empty_text);
}
void bind(@StringRes int title) {
text.setText(title);
}
}
interface EventListener {
void onStickerPackClicked(@NonNull String packId, @NonNull String packKey);
void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey);
void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey);
void onStickerPackShareClicked(@NonNull String packId, @NonNull String packKey);
}
private static class NoSectionException extends IllegalStateException {}
}

View File

@ -1,124 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase.StickerPackRecordReader;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.List;
final class StickerManagementRepository {
private final Context context;
private final StickerDatabase stickerDatabase;
private final AttachmentDatabase attachmentDatabase;
StickerManagementRepository(@NonNull Context context) {
this.context = context.getApplicationContext();
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
}
void deleteOrphanedStickerPacks() {
SignalExecutors.SERIAL.execute(stickerDatabase::deleteOrphanedPacks);
}
void fetchUnretrievedReferencePacks() {
SignalExecutors.SERIAL.execute(() -> {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
try (Cursor cursor = attachmentDatabase.getUnavailableStickerPacks()) {
while (cursor != null && cursor.moveToNext()) {
String packId = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.STICKER_PACK_ID));
String packKey = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.STICKER_PACK_KEY));
jobManager.add(new StickerPackDownloadJob(packId, packKey, true));
}
}
});
}
void getStickerPacks(@NonNull Callback<PackResult> callback) {
SignalExecutors.SERIAL.execute(() -> {
List<StickerPackRecord> installedPacks = new ArrayList<>();
List<StickerPackRecord> availablePacks = new ArrayList<>();
List<StickerPackRecord> blessedPacks = new ArrayList<>();
try (StickerPackRecordReader reader = new StickerPackRecordReader(stickerDatabase.getAllStickerPacks())) {
StickerPackRecord record;
while ((record = reader.getNext()) != null) {
if (record.isInstalled()) {
installedPacks.add(record);
} else if (BlessedPacks.contains(record.getPackId())) {
blessedPacks.add(record);
} else {
availablePacks.add(record);
}
}
}
callback.onComplete(new PackResult(installedPacks, availablePacks, blessedPacks));
});
}
void uninstallStickerPack(@NonNull String packId, @NonNull String packKey) {
SignalExecutors.SERIAL.execute(() -> {
stickerDatabase.uninstallPack(packId);
});
}
void installStickerPack(@NonNull String packId, @NonNull String packKey) {
SignalExecutors.SERIAL.execute(() -> {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
if (stickerDatabase.isPackAvailableAsReference(packId)) {
stickerDatabase.markPackAsInstalled(packId);
}
jobManager.add(new StickerPackDownloadJob(packId, packKey, false));
});
}
static class PackResult {
private final List<StickerPackRecord> installedPacks;
private final List<StickerPackRecord> availablePacks;
private final List<StickerPackRecord> blessedPacks;
PackResult(@NonNull List<StickerPackRecord> installedPacks,
@NonNull List<StickerPackRecord> availablePacks,
@NonNull List<StickerPackRecord> blessedPacks)
{
this.installedPacks = installedPacks;
this.availablePacks = availablePacks;
this.blessedPacks = blessedPacks;
}
@NonNull List<StickerPackRecord> getInstalledPacks() {
return installedPacks;
}
@NonNull List<StickerPackRecord> getAvailablePacks() {
return availablePacks;
}
@NonNull List<StickerPackRecord> getBlessedPacks() {
return blessedPacks;
}
}
interface Callback<T> {
void onComplete(T result);
}
}

View File

@ -1,80 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.stickers.StickerManagementRepository.PackResult;
final class StickerManagementViewModel extends ViewModel {
private final Application application;
private final StickerManagementRepository repository;
private final MutableLiveData<PackResult> packs;
private final ContentObserver observer;
private StickerManagementViewModel(@NonNull Application application, @NonNull StickerManagementRepository repository) {
this.application = application;
this.repository = repository;
this.packs = new MutableLiveData<>();
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
repository.deleteOrphanedStickerPacks();
repository.getStickerPacks(packs::postValue);
}
};
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, observer);
}
void init() {
repository.deleteOrphanedStickerPacks();
repository.fetchUnretrievedReferencePacks();
}
void onVisible() {
repository.deleteOrphanedStickerPacks();
}
@NonNull LiveData<PackResult> getStickerPacks() {
repository.getStickerPacks(packs::postValue);
return packs;
}
void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey) {
repository.uninstallStickerPack(packId, packKey);
}
void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey) {
repository.installStickerPack(packId, packKey);
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(observer);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerManagementRepository repository;
Factory(@NonNull Application application, @NonNull StickerManagementRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new StickerManagementViewModel(application, repository));
}
}
}

View File

@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsignal.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.List;
/**
* Local model that represents the data present in the libsignal model
* {@link org.session.libsignal.service.api.messages.SignalServiceStickerManifest}.
*/
public final class StickerManifest {
private final String packId;
private final String packKey;
private final Optional<String> title;
private final Optional<String> author;
private final Optional<Sticker> cover;
private final List<Sticker> stickers;
public StickerManifest(@NonNull String packId,
@NonNull String packKey,
@NonNull Optional<String> title,
@NonNull Optional<String> author,
@NonNull Optional<Sticker> cover,
@NonNull List<Sticker> stickers)
{
this.packId = packId;
this.packKey = packKey;
this.title = title;
this.author = author;
this.cover = cover;
this.stickers = new ArrayList<>(stickers);
}
public @NonNull String getPackId() {
return packId;
}
public @NonNull String getPackKey() {
return packKey;
}
public @NonNull Optional<String> getTitle() {
return title;
}
public @NonNull Optional<String> getAuthor() {
return author;
}
public @NonNull Optional<Sticker> getCover() {
return cover;
}
public @NonNull List<Sticker> getStickers() {
return stickers;
}
public static class Sticker {
private final String packId;
private final String packKey;
private final int id;
private final String emoji;
private final Optional<Uri> uri;
public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji) {
this(packId, packKey, id, emoji, null);
}
public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji, @Nullable Uri uri) {
this.packId = packId;
this.packKey = packKey;
this.id = id;
this.emoji = emoji;
this.uri = Optional.fromNullable(uri);
}
public @NonNull String getPackId() {
return packId;
}
public @NonNull String getPackKey() {
return packKey;
}
public int getId() {
return id;
}
public String getEmoji() {
return emoji;
}
public Optional<Uri> getUri() {
return uri;
}
}
}

View File

@ -1,15 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
public class StickerPackInstallEvent {
private final Object iconGlideModel;
public StickerPackInstallEvent(@NonNull Object iconGlideModel) {
this.iconGlideModel = iconGlideModel;
}
public @NonNull Object getIconGlideModel() {
return iconGlideModel;
}
}

View File

@ -1,218 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import org.session.libsignal.libsignal.util.Pair;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
import org.session.libsession.utilities.concurrent.SimpleTask;
import network.loki.messenger.R;
/**
* Shows the contents of a pack and allows the user to install it (if not installed) or remove it
* (if installed). This is also the handler for sticker pack deep links.
*/
public final class StickerPackPreviewActivity extends PassphraseRequiredActionBarActivity {
private static final String TAG = Log.tag(StickerPackPreviewActivity.class);
private StickerPackPreviewViewModel viewModel;
private ImageView coverImage;
private TextView stickerTitle;
private TextView stickerAuthor;
private View installButton;
private View removeButton;
private RecyclerView stickerList;
private View shareButton;
private View shareButtonImage;
private StickerPackPreviewAdapter adapter;
private GridLayoutManager layoutManager;
public static Intent getIntent(@NonNull String packId, @NonNull String packKey) {
Intent intent = new Intent(Intent.ACTION_VIEW, StickerUrl.createActionUri(packId, packKey));
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
setContentView(R.layout.sticker_preview_activity);
Optional<Pair<String, String>> stickerParams = StickerUrl.parseActionUri(getIntent().getData());
if (!stickerParams.isPresent()) {
Log.w(TAG, "Invalid URI!");
finish();
return;
}
String packId = stickerParams.get().first();
String packKey = stickerParams.get().second();
initToolbar();
initView();
initViewModel(packId, packKey);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
onScreenWidthChanged(getScreenWidth());
}
private void initView() {
this.coverImage = findViewById(R.id.sticker_install_cover);
this.stickerTitle = findViewById(R.id.sticker_install_title);
this.stickerAuthor = findViewById(R.id.sticker_install_author);
this.installButton = findViewById(R.id.sticker_install_button);
this.removeButton = findViewById(R.id.sticker_install_remove_button);
this.stickerList = findViewById(R.id.sticker_install_list);
this.shareButton = findViewById(R.id.sticker_install_share_button);
this.shareButtonImage = findViewById(R.id.sticker_install_share_button_image);
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this));
this.layoutManager = new GridLayoutManager(this, 2);
onScreenWidthChanged(getScreenWidth());
stickerList.setLayoutManager(layoutManager);
stickerList.setAdapter(adapter);
}
private void initToolbar() {
Toolbar toolbar = findViewById(R.id.sticker_install_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.StickerPackPreviewActivity_stickers);
toolbar.setNavigationOnClickListener(v -> onBackPressed());
}
private void initViewModel(@NonNull String packId, @NonNull String packKey) {
viewModel = ViewModelProviders.of(this, new StickerPackPreviewViewModel.Factory(getApplication(),
new StickerPackPreviewRepository(this),
new StickerManagementRepository(this)))
.get(StickerPackPreviewViewModel.class);
viewModel.getStickerManifest(packId, packKey).observe(this, manifest -> {
if (manifest == null) return;
if (manifest.isPresent()) {
presentManifest(manifest.get().getManifest());
presentButton(manifest.get().isInstalled());
presentShareButton(manifest.get().isInstalled(), manifest.get().getManifest().getPackId(), manifest.get().getManifest().getPackKey());
} else {
presentError();
}
});
}
private void presentManifest(@NonNull StickerManifest manifest) {
stickerTitle.setText(manifest.getTitle().or(getString(R.string.StickerPackPreviewActivity_untitled)));
stickerAuthor.setText(manifest.getAuthor().or(getString(R.string.StickerPackPreviewActivity_unknown)));
adapter.setStickers(manifest.getStickers());
installButton.setOnClickListener(v -> {
SimpleTask.run(() -> {
ApplicationContext.getInstance(this)
.getJobManager()
.add(new StickerPackDownloadJob(manifest.getPackId(), manifest.getPackKey(), false));
return null;
}, (nothing) -> finish());
});
Sticker first = manifest.getStickers().isEmpty() ? null : manifest.getStickers().get(0);
Sticker cover = manifest.getCover().or(Optional.fromNullable(first)).orNull();
if (cover != null) {
Object model = cover.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(cover.getUri().get())
: new StickerRemoteUri(cover.getPackId(), cover.getPackKey(), cover.getId());
GlideApp.with(this).load(model)
.transition(DrawableTransitionOptions.withCrossFade())
.into(coverImage);
} else {
coverImage.setImageDrawable(null);
}
}
private void presentButton(boolean installed) {
if (installed) {
removeButton.setVisibility(View.VISIBLE);
removeButton.setOnClickListener(v -> {
viewModel.onRemoveClicked();
finish();
});
installButton.setVisibility(View.GONE);
installButton.setOnClickListener(null);
} else {
installButton.setVisibility(View.VISIBLE);
installButton.setOnClickListener(v -> {
viewModel.onInstallClicked();
finish();
});
removeButton.setVisibility(View.GONE);
removeButton.setOnClickListener(null);
}
}
private void presentShareButton(boolean installed, @NonNull String packId, @NonNull String packKey) {
if (installed) {
shareButton.setVisibility(View.VISIBLE);
shareButtonImage.setVisibility(View.VISIBLE);
shareButton.setOnClickListener(v -> {
Intent composeIntent = new Intent(this, ShareActivity.class);
composeIntent.putExtra(Intent.EXTRA_TEXT, StickerUrl.createShareLink(packId, packKey));
startActivity(composeIntent);
finish();
});
} else {
shareButton.setVisibility(View.GONE);
shareButtonImage.setVisibility(View.GONE);
shareButton.setOnClickListener(null);
}
}
private void presentError() {
Toast.makeText(this, R.string.StickerPackPreviewActivity_failed_to_load_sticker_pack, Toast.LENGTH_SHORT).show();
finish();
}
private void onScreenWidthChanged(int newWidth) {
if (layoutManager != null) {
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelOffset(R.dimen.sticker_preview_sticker_size));
}
}
private int getScreenWidth() {
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
return size.x;
}
}

View File

@ -1,69 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.ArrayList;
import java.util.List;
import network.loki.messenger.R;
public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<StickerPackPreviewAdapter.StickerViewHolder> {
private final GlideRequests glideRequests;
private final List<StickerManifest.Sticker> list;
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests) {
this.glideRequests = glideRequests;
this.list = new ArrayList<>();
}
@Override
public @NonNull StickerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new StickerViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_preview_list_item, viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull StickerViewHolder stickerViewHolder, int i) {
stickerViewHolder.bind(glideRequests, list.get(i));
}
@Override
public int getItemCount() {
return list.size();
}
void setStickers(List<StickerManifest.Sticker> stickers) {
list.clear();
list.addAll(stickers);
notifyDataSetChanged();
}
static class StickerViewHolder extends RecyclerView.ViewHolder {
private final ImageView image;
private StickerViewHolder(@NonNull View itemView) {
super(itemView);
this.image = itemView.findViewById(R.id.sticker_install_item_image);
}
void bind(@NonNull GlideRequests glideRequests, @NonNull StickerManifest.Sticker sticker) {
Object model = sticker.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(sticker.getUri().get())
: new StickerRemoteUri(sticker.getPackId(), sticker.getPackKey(), sticker.getId());
glideRequests.load(model)
.transition(DrawableTransitionOptions.withCrossFade())
.into(image);
}
}
}

View File

@ -1,161 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.utilities.Hex;
import org.session.libsession.utilities.concurrent.SignalExecutors;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.messages.SignalServiceStickerManifest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
public final class StickerPackPreviewRepository implements InjectableType {
private static final String TAG = Log.tag(StickerPackPreviewRepository.class);
private final StickerDatabase stickerDatabase;
@Inject SignalServiceMessageReceiver receiver;
public StickerPackPreviewRepository(@NonNull Context context) {
ApplicationContext.getInstance(context).injectDependencies(this);
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
}
public void getStickerManifest(@NonNull String packId,
@NonNull String packKey,
@NonNull Callback<Optional<StickerManifestResult>> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
Optional<StickerManifestResult> localManifest = getManifestFromDatabase(packId);
if (localManifest.isPresent()) {
Log.d(TAG, "Found manifest locally.");
callback.onComplete(localManifest);
} else {
Log.d(TAG, "Looking for manifest remotely.");
callback.onComplete(getManifestRemote(packId, packKey));
}
});
}
@WorkerThread
private Optional<StickerManifestResult> getManifestFromDatabase(@NonNull String packId) {
StickerPackRecord record = stickerDatabase.getStickerPack(packId);
if (record != null && record.isInstalled()) {
StickerManifest.Sticker cover = toSticker(record.getCover());
List<StickerManifest.Sticker> stickers = getStickersFromDatabase(packId);
StickerManifest manifest = new StickerManifest(record.getPackId(),
record.getPackKey(),
record.getTitle(),
record.getAuthor(),
Optional.of(cover),
stickers);
return Optional.of(new StickerManifestResult(manifest, record.isInstalled()));
}
return Optional.absent();
}
@WorkerThread
private Optional<StickerManifestResult> getManifestRemote(@NonNull String packId, @NonNull String packKey) {
try {
byte[] packIdBytes = Hex.fromStringCondensed(packId);
byte[] packKeyBytes = Hex.fromStringCondensed(packKey);
SignalServiceStickerManifest remoteManifest = receiver.retrieveStickerManifest(packIdBytes, packKeyBytes);
StickerManifest localManifest = new StickerManifest(packId,
packKey,
remoteManifest.getTitle(),
remoteManifest.getAuthor(),
toOptionalSticker(packId, packKey, remoteManifest.getCover()),
Stream.of(remoteManifest.getStickers())
.map(s -> toSticker(packId, packKey, s))
.toList());
return Optional.of(new StickerManifestResult(localManifest, false));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, "Failed to retrieve pack manifest.", e);
}
return Optional.absent();
}
@WorkerThread
private List<StickerManifest.Sticker> getStickersFromDatabase(@NonNull String packId) {
List<StickerManifest.Sticker> stickers = new ArrayList<>();
try (Cursor cursor = stickerDatabase.getStickersForPack(packId)) {
StickerDatabase.StickerRecordReader reader = new StickerDatabase.StickerRecordReader(cursor);
StickerRecord record;
while ((record = reader.getNext()) != null) {
stickers.add(toSticker(record));
}
}
return stickers;
}
private Optional<StickerManifest.Sticker> toOptionalSticker(@NonNull String packId,
@NonNull String packKey,
@NonNull Optional<SignalServiceStickerManifest.StickerInfo> remoteSticker)
{
return remoteSticker.isPresent() ? Optional.of(toSticker(packId, packKey, remoteSticker.get()))
: Optional.absent();
}
private StickerManifest.Sticker toSticker(@NonNull String packId,
@NonNull String packKey,
@NonNull SignalServiceStickerManifest.StickerInfo remoteSticker)
{
return new StickerManifest.Sticker(packId, packKey, remoteSticker.getId(), remoteSticker.getEmoji());
}
private StickerManifest.Sticker toSticker(@NonNull StickerRecord record) {
return new StickerManifest.Sticker(record.getPackId(), record.getPackKey(), record.getStickerId(), record.getEmoji(), record.getUri());
}
static class StickerManifestResult {
private final StickerManifest manifest;
private final boolean isInstalled;
StickerManifestResult(StickerManifest manifest, boolean isInstalled) {
this.manifest = manifest;
this.isInstalled = isInstalled;
}
public StickerManifest getManifest() {
return manifest;
}
public boolean isInstalled() {
return isInstalled;
}
}
interface Callback<T> {
void onComplete(T result);
}
}

View File

@ -1,90 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewRepository.StickerManifestResult;
import org.session.libsignal.libsignal.util.guava.Optional;
final class StickerPackPreviewViewModel extends ViewModel {
private final Application application;
private final StickerPackPreviewRepository previewRepository;
private final StickerManagementRepository managementRepository;
private final MutableLiveData<Optional<StickerManifestResult>> stickerManifest;
private final ContentObserver packObserver;
private String packId;
private String packKey;
private StickerPackPreviewViewModel(@NonNull Application application,
@NonNull StickerPackPreviewRepository previewRepository,
@NonNull StickerManagementRepository managementRepository)
{
this.application = application;
this.previewRepository = previewRepository;
this.managementRepository = managementRepository;
this.stickerManifest = new MutableLiveData<>();
this.packObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
if (!TextUtils.isEmpty(packId) && !TextUtils.isEmpty(packKey)) {
previewRepository.getStickerManifest(packId, packKey, stickerManifest::postValue);
}
}
};
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
}
LiveData<Optional<StickerManifestResult>> getStickerManifest(@NonNull String packId, @NonNull String packKey) {
this.packId = packId;
this.packKey = packKey;
previewRepository.getStickerManifest(packId, packKey, stickerManifest::postValue);
return stickerManifest;
}
void onInstallClicked() {
managementRepository.installStickerPack(packId, packKey);
}
void onRemoveClicked() {
managementRepository.uninstallStickerPack(packId, packKey);
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(packObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerPackPreviewRepository previewRepository;
private final StickerManagementRepository managementRepository;
Factory(@NonNull Application application,
@NonNull StickerPackPreviewRepository previewRepository,
@NonNull StickerManagementRepository managementRepository)
{
this.application = application;
this.previewRepository = previewRepository;
this.managementRepository = managementRepository;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new StickerPackPreviewViewModel(application, previewRepository, managementRepository));
}
}
}

View File

@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import network.loki.messenger.R;
/**
* A popup that shows a given sticker fullscreen.
*/
final class StickerPreviewPopup extends PopupWindow {
private final GlideRequests glideRequests;
private final ImageView image;
StickerPreviewPopup(@NonNull Context context, @NonNull GlideRequests glideRequests) {
super(LayoutInflater.from(context).inflate(R.layout.sticker_preview_popup, null),
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
this.glideRequests = glideRequests;
this.image = getContentView().findViewById(R.id.sticker_popup_image);
setTouchable(false);
}
void presentSticker(@NonNull StickerRecord stickerRecord) {
glideRequests.load(new DecryptableUri(stickerRecord.getUri()))
.into(image);
}
}

View File

@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Key;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.Objects;
/**
* Used as a model to be given to Glide for a sticker that isn't present locally.
*/
public final class StickerRemoteUri implements Key {
private final String packId;
private final String packKey;
private final int stickerId;
public StickerRemoteUri(@NonNull String packId, @NonNull String packKey, int stickerId) {
this.packId = packId;
this.packKey = packKey;
this.stickerId = stickerId;
}
public @NonNull String getPackId() {
return packId;
}
public @NonNull String getPackKey() {
return packKey;
}
public int getStickerId() {
return stickerId;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(packId.getBytes());
messageDigest.update(packKey.getBytes());
messageDigest.update(ByteBuffer.allocate(4).putInt(stickerId).array());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StickerRemoteUri that = (StickerRemoteUri) o;
return stickerId == that.stickerId &&
Objects.equals(packId, that.packId) &&
Objects.equals(packKey, that.packKey);
}
@Override
public int hashCode() {
return Objects.hash(packId, packKey, stickerId);
}
}

View File

@ -1,64 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import java.io.IOException;
import java.io.InputStream;
/**
* Downloads a sticker remotely. Used with Glide.
*/
public final class StickerRemoteUriFetcher implements DataFetcher<InputStream> {
private static final String TAG = Log.tag(StickerRemoteUriFetcher.class);
private final SignalServiceMessageReceiver receiver;
private final StickerRemoteUri stickerUri;
public StickerRemoteUriFetcher(@NonNull SignalServiceMessageReceiver receiver, @NonNull StickerRemoteUri stickerUri) {
this.receiver = receiver;
this.stickerUri = stickerUri;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
byte[] packIdBytes = Hex.fromStringCondensed(stickerUri.getPackId());
byte[] packKeyBytes = Hex.fromStringCondensed(stickerUri.getPackKey());
InputStream stream = receiver.retrieveSticker(packIdBytes, packKeyBytes, stickerUri.getStickerId());
callback.onDataReady(stream);
} catch (IOException | InvalidMessageException e) {
callback.onLoadFailed(e);
}
}
@Override
public void cleanup() {
}
@Override
public void cancel() {
Log.d(TAG, "Canceled.");
}
@Override
public @NonNull Class<InputStream> getDataClass() {
return InputStream.class;
}
@Override
public @NonNull DataSource getDataSource() {
return DataSource.REMOTE;
}
}

View File

@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import java.io.InputStream;
import javax.inject.Inject;
/**
* Glide loader to fetch a sticker remotely.
*/
public final class StickerRemoteUriLoader implements ModelLoader<StickerRemoteUri, InputStream> {
private final SignalServiceMessageReceiver receiver;
public StickerRemoteUriLoader(@NonNull SignalServiceMessageReceiver receiver) {
this.receiver = receiver;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull StickerRemoteUri sticker, int width, int height, @NonNull Options options) {
return new LoadData<>(sticker, new StickerRemoteUriFetcher(receiver, sticker));
}
@Override
public boolean handles(@NonNull StickerRemoteUri sticker) {
return true;
}
public static class Factory implements ModelLoaderFactory<StickerRemoteUri, InputStream>, InjectableType {
@Inject SignalServiceMessageReceiver receiver;
public Factory(@NonNull Context context) {
ApplicationContext.getInstance(context).injectDependencies(this);
}
@Override
public @NonNull ModelLoader<StickerRemoteUri, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new StickerRemoteUriLoader(receiver);
}
@Override
public void teardown() {
}
}
}

View File

@ -1,66 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.session.libsession.utilities.concurrent.SignalExecutors;
public final class StickerSearchRepository {
private final StickerDatabase stickerDatabase;
private final AttachmentDatabase attachmentDatabase;
public StickerSearchRepository(@NonNull Context context) {
this.stickerDatabase = DatabaseFactory.getStickerDatabase(context);
this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
}
public void searchByEmoji(@NonNull String emoji, @NonNull Callback<CursorList<StickerRecord>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
Cursor cursor = stickerDatabase.getStickersByEmoji(emoji);
if (cursor != null) {
callback.onResult(new CursorList<>(cursor, new StickerModelBuilder()));
} else {
callback.onResult(CursorList.emptyList());
}
});
}
public void getStickerFeatureAvailability(@NonNull Callback<Boolean> callback) {
SignalExecutors.BOUNDED.execute(() -> {
try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) {
if (cursor != null && cursor.moveToFirst()) {
callback.onResult(true);
} else {
callback.onResult(attachmentDatabase.hasStickerAttachments());
}
}
});
}
private static class StickerModelBuilder implements CursorList.ModelBuilder<StickerRecord> {
@Override
public StickerRecord build(@NonNull Cursor cursor) {
return new StickerDatabase.StickerRecordReader(cursor).getCurrent();
}
}
private static class StickerPackModelBuilder implements CursorList.ModelBuilder<StickerPackRecord> {
@Override
public StickerPackRecord build(@NonNull Cursor cursor) {
return new StickerDatabase.StickerPackRecordReader(cursor).getCurrent();
}
}
public interface Callback<T> {
void onResult(T result);
}
}

View File

@ -1,57 +0,0 @@
package org.thoughtcrime.securesms.stickers;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.session.libsignal.libsignal.util.Pair;
import org.session.libsignal.libsignal.util.guava.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Manages creating and parsing the various sticker pack URLs.
*/
public class StickerUrl {
private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^https://signal\\.org/addstickers/#pack_id=(.*)&pack_key=(.*)$");
public static Optional<Pair<String, String>> parseActionUri(@Nullable Uri uri) {
if (uri == null) return Optional.absent();
String packId = uri.getQueryParameter("pack_id");
String packKey = uri.getQueryParameter("pack_key");
if (TextUtils.isEmpty(packId) || TextUtils.isEmpty(packKey)) {
return Optional.absent();
}
return Optional.of(new Pair<>(packId, packKey));
}
public static @NonNull Uri createActionUri(@NonNull String packId, @NonNull String packKey) {
return Uri.parse(String.format("sgnl://addstickers?pack_id=%s&pack_key=%s", packId, packKey));
}
public static boolean isValidShareLink(@Nullable String url) {
return parseShareLink(url).isPresent();
}
public static @NonNull Optional<Pair<String, String>> parseShareLink(@Nullable String url) {
if (url == null) return Optional.absent();
Matcher matcher = STICKER_URL_PATTERN.matcher(url);
if (matcher.matches() && matcher.groupCount() == 2) {
return Optional.of(new Pair<>(matcher.group(1), matcher.group(2)));
}
return Optional.absent();
}
public static String createShareLink(@NonNull String packId, @NonNull String packKey) {
return "https://signal.org/addstickers/#pack_id=" + packId + "&pack_key=" + packKey;
}
}

View File

@ -116,10 +116,6 @@ object TextSecurePreferences {
private const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
private const val SEEN_STICKER_INTRO_TOOLTIP = "pref_seen_sticker_intro_tooltip"
private const val MEDIA_KEYBOARD_MODE = "pref_media_keyboard_mode"
// region FCM
const val IS_USING_FCM = "pref_is_using_fcm"
private const val FCM_TOKEN = "pref_fcm_token"
@ -764,27 +760,6 @@ object TextSecurePreferences {
setIntegerPrefrence(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
}
@JvmStatic
fun hasSeenStickerIntroTooltip(context: Context): Boolean {
return getBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, false)
}
@JvmStatic
fun setHasSeenStickerIntroTooltip(context: Context, seenStickerTooltip: Boolean) {
setBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, seenStickerTooltip)
}
@JvmStatic
fun setMediaKeyboardMode(context: Context, mode: MediaKeyboardMode) {
setStringPreference(context, MEDIA_KEYBOARD_MODE, mode.name)
}
@JvmStatic
fun getMediaKeyboardMode(context: Context): MediaKeyboardMode {
val name = getStringPreference(context, MEDIA_KEYBOARD_MODE, MediaKeyboardMode.EMOJI.name)!!
return MediaKeyboardMode.valueOf(name)
}
@JvmStatic
fun setBooleanPreference(context: Context, key: String?, value: Boolean) {
getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()

View File

@ -864,7 +864,7 @@ public class SignalServiceMessageSender {
String body = Base64.encodeBytes(ciphertext);
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
OutgoingPushMessage message = new OutgoingPushMessage(type, 1, 0, body);
OutgoingPushMessage message = new OutgoingPushMessage(type, body);
messages.add(message);
return new OutgoingPushMessageList(publicKey, timestamp, messages, false);

View File

@ -1,91 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.service.internal.crypto;
import com.google.protobuf.ByteString;
import org.session.libsignal.libsignal.InvalidKeyException;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.libsignal.kdf.HKDFv3;
import org.session.libsignal.service.internal.util.Util;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import static org.session.libsignal.service.internal.push.ProvisioningProtos.ProvisionEnvelope;
import static org.session.libsignal.service.internal.push.ProvisioningProtos.ProvisionMessage;
public class ProvisioningCipher {
private static final String TAG = ProvisioningCipher.class.getSimpleName();
private final ECPublicKey theirPublicKey;
public ProvisioningCipher(ECPublicKey theirPublicKey) {
this.theirPublicKey = theirPublicKey;
}
public byte[] encrypt(ProvisionMessage message) throws InvalidKeyException {
ECKeyPair ourKeyPair = Curve.generateKeyPair();
byte[] sharedSecret = Curve.calculateAgreement(theirPublicKey, ourKeyPair.getPrivateKey());
byte[] derivedSecret = new HKDFv3().deriveSecrets(sharedSecret, "TextSecure Provisioning Message".getBytes(), 64);
byte[][] parts = Util.split(derivedSecret, 32, 32);
byte[] version = {0x01};
byte[] ciphertext = getCiphertext(parts[0], message.toByteArray());
byte[] mac = getMac(parts[1], Util.join(version, ciphertext));
byte[] body = Util.join(version, ciphertext, mac);
return ProvisionEnvelope.newBuilder()
.setPublicKey(ByteString.copyFrom(ourKeyPair.getPublicKey().serialize()))
.setBody(ByteString.copyFrom(body))
.build()
.toByteArray();
}
private byte[] getCiphertext(byte[] key, byte[] message) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
return Util.join(cipher.getIV(), cipher.doFinal(message));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getMac(byte[] key, byte[] message) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(message);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -14,20 +14,11 @@ public class OutgoingPushMessage {
@JsonProperty
public int type;
@JsonProperty
private int destinationDeviceId;
@JsonProperty
private int destinationRegistrationId;
@JsonProperty
public String content;
public OutgoingPushMessage(int type,
int destinationDeviceId,
int destinationRegistrationId,
String content)
public OutgoingPushMessage(int type, String content)
{
this.type = type;
this.destinationDeviceId = destinationDeviceId;
this.destinationRegistrationId = destinationRegistrationId;
this.content = content;
}
}