diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7af05a91f..cb0a0d3fe 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -10,6 +10,9 @@
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
index 10f1f2021..be3425a01 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
@@ -76,6 +76,7 @@ public abstract class Database {
}
protected void notifyStickerPackListeners() {
+ ApplicationDependencies.getDatabaseObserver().notifyStickerPackObservers();
context.getContentResolver().notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java
index f27dcd73d..b2723d180 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java
@@ -30,6 +30,7 @@ public final class DatabaseObserver {
private final Map> paymentObservers;
private final Set allPaymentsObservers;
private final Set chatColorsObservers;
+ private final Set stickerPackObservers;
public DatabaseObserver(Application application) {
this.application = application;
@@ -40,6 +41,7 @@ public final class DatabaseObserver {
this.paymentObservers = new HashMap<>();
this.allPaymentsObservers = new HashSet<>();
this.chatColorsObservers = new HashSet<>();
+ this.stickerPackObservers = new HashSet<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -78,6 +80,12 @@ public final class DatabaseObserver {
});
}
+ public void registerStickerPackObserver(@NonNull Observer listener) {
+ executor.execute(() -> {
+ stickerPackObservers.add(listener);
+ });
+ }
+
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
@@ -85,6 +93,7 @@ public final class DatabaseObserver {
unregisterMapped(verboseConversationObservers, listener);
unregisterMapped(paymentObservers, listener);
chatColorsObservers.remove(listener);
+ stickerPackObservers.remove(listener);
});
}
@@ -160,6 +169,12 @@ public final class DatabaseObserver {
});
}
+ public void notifyStickerPackObservers() {
+ executor.execute(() -> {
+ notifySet(stickerPackObservers);
+ });
+ }
+
private void registerMapped(@NonNull Map> map, @NonNull K key, @NonNull Observer listener) {
Set listeners = map.get(key);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt
new file mode 100644
index 000000000..8937c67bd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.content.Context
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.database.model.StickerRecord
+import org.thoughtcrime.securesms.glide.cache.ApngOptions
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
+import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingModel
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+class KeyboardStickerListAdapter(
+ private val glideRequests: GlideRequests,
+ private val eventListener: EventListener?,
+ private val allowApngAnimation: Boolean,
+) : MappingAdapter() {
+
+ init {
+ registerFactory(Sticker::class.java, LayoutFactory(::StickerViewHolder, R.layout.sticker_keyboard_page_list_item))
+ registerFactory(StickerHeader::class.java, LayoutFactory(::StickerHeaderViewHolder, R.layout.sticker_grid_header))
+ }
+
+ data class Sticker(override val packId: String, val stickerRecord: StickerRecord) : MappingModel, HasPackId {
+ val uri: DecryptableUri
+ get() = DecryptableUri(stickerRecord.uri)
+
+ override fun areItemsTheSame(newItem: Sticker): Boolean {
+ return packId == newItem.packId && stickerRecord.rowId == newItem.stickerRecord.rowId
+ }
+
+ override fun areContentsTheSame(newItem: Sticker): Boolean {
+ return areItemsTheSame(newItem)
+ }
+ }
+
+ private inner class StickerViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val image: ImageView = findViewById(R.id.sticker_keyboard_page_image)
+
+ override fun bind(model: Sticker) {
+ glideRequests.load(model.uri)
+ .set(ApngOptions.ANIMATE, allowApngAnimation)
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(image)
+
+ if (eventListener != null) {
+ itemView.setOnClickListener { eventListener.onStickerClicked(model) }
+ itemView.setOnLongClickListener {
+ eventListener.onStickerLongClicked(model)
+ true
+ }
+ } else {
+ itemView.setOnClickListener(null)
+ itemView.setOnLongClickListener(null)
+ }
+ }
+ }
+
+ data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel, HasPackId {
+ fun getTitle(context: Context): String {
+ return title ?: context.resources.getString(titleResource ?: R.string.StickerManagementAdapter_untitled)
+ }
+
+ override fun areItemsTheSame(newItem: StickerHeader): Boolean {
+ return title == newItem.title
+ }
+
+ override fun areContentsTheSame(newItem: StickerHeader): Boolean {
+ return areItemsTheSame(newItem)
+ }
+ }
+
+ private class StickerHeaderViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val title: TextView = findViewById(R.id.sticker_grid_header_title)
+
+ override fun bind(model: StickerHeader) {
+ title.text = model.getTitle(context)
+ }
+ }
+
+ interface HasPackId {
+ val packId: String
+ }
+
+ interface EventListener {
+ fun onStickerClicked(sticker: Sticker)
+ fun onStickerLongClicked(sticker: Sticker)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt
index 29f89ebd0..e8c2d326a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt
@@ -2,38 +2,84 @@ package org.thoughtcrime.securesms.keyboard.sticker
import android.os.Bundle
import android.view.View
+import androidx.annotation.Px
import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
-import androidx.viewpager.widget.PagerAdapter
-import androidx.viewpager.widget.ViewPager
+import androidx.recyclerview.widget.RecyclerView.SmoothScroller
import com.google.android.material.appbar.AppBarLayout
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.components.emoji.MediaKeyboardBottomTabAdapter
-import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider
+import org.thoughtcrime.securesms.database.DatabaseObserver
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
+import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.mms.GlideApp
-import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider.StickerEventListener
+import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener
+import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener.RolloverStickerRetriever
+import org.thoughtcrime.securesms.util.DeviceProperties
+import org.thoughtcrime.securesms.util.MappingModel
+import org.thoughtcrime.securesms.util.Throttler
+import org.whispersystems.libsignal.util.Pair
+import java.util.Optional
+import kotlin.math.abs
-class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment) {
+class StickerKeyboardPageFragment :
+ LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment),
+ KeyboardStickerListAdapter.EventListener,
+ StickerRolloverTouchListener.RolloverEventListener,
+ RolloverStickerRetriever,
+ DatabaseObserver.Observer,
+ View.OnLayoutChangeListener {
- private val presenter: StickerPresenter = StickerPresenter()
- private lateinit var provider: StickerKeyboardProvider
-
- private lateinit var stickerPager: ViewPager
+ private lateinit var stickerList: RecyclerView
+ private lateinit var keyboardStickerListAdapter: KeyboardStickerListAdapter
+ private lateinit var layoutManager: GridLayoutManager
+ private lateinit var listTouchListener: StickerRolloverTouchListener
private lateinit var stickerPacksRecycler: RecyclerView
- private lateinit var manageStickers: View
- private lateinit var tabAdapter: MediaKeyboardBottomTabAdapter
+ private lateinit var appBarLayout: AppBarLayout
+ private lateinit var stickerPacksAdapter: StickerPackListAdapter
private lateinit var viewModel: StickerKeyboardPageViewModel
+ private val packIdSelectionOnScroll: UpdatePackSelectionOnScroll = UpdatePackSelectionOnScroll()
+ private val observerThrottler: Throttler = Throttler(500)
+ private val stickerThrottler: Throttler = Throttler(100)
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- stickerPager = view.findViewById(R.id.sticker_pager)
- manageStickers = view.findViewById(R.id.sticker_manage)
+
+ val glideRequests = GlideApp.with(this)
+ keyboardStickerListAdapter = KeyboardStickerListAdapter(glideRequests, this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()))
+ layoutManager = GridLayoutManager(requireContext(), 2).apply {
+ spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+ override fun getSpanSize(position: Int): Int {
+ val model: Optional> = keyboardStickerListAdapter.getModel(position)
+ if (model.isPresent && model.get() is KeyboardStickerListAdapter.StickerHeader) {
+ return spanCount
+ }
+ return 1
+ }
+ }
+ }
+ listTouchListener = StickerRolloverTouchListener(requireContext(), glideRequests, this, this)
+
+ stickerList = view.findViewById(R.id.sticker_keyboard_list)
+ stickerList.layoutManager = layoutManager
+ stickerList.adapter = keyboardStickerListAdapter
+ stickerList.addOnItemTouchListener(listTouchListener)
+ stickerList.addOnScrollListener(packIdSelectionOnScroll)
+
stickerPacksRecycler = view.findViewById(R.id.sticker_packs_recycler)
+ stickerPacksAdapter = StickerPackListAdapter(glideRequests, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()), this::onTabSelected)
+ stickerPacksRecycler.adapter = stickerPacksAdapter
+
+ appBarLayout = view.findViewById(R.id.sticker_keyboard_search_appbar)
+
view.findViewById(R.id.sticker_keyboard_search_text).callbacks = object : KeyboardPageSearchView.Callbacks {
override fun onClicked() {
StickerSearchDialogFragment.show(requireActivity().supportFragmentManager)
@@ -41,70 +87,139 @@ class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_stic
}
view.findViewById(R.id.sticker_search).setOnClickListener { StickerSearchDialogFragment.show(requireActivity().supportFragmentManager) }
+ view.findViewById(R.id.sticker_manage).setOnClickListener { findListener()?.onStickerManagementClicked() }
- view.findViewById(R.id.sticker_appbar).setExpanded(false)
+ ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(this)
+
+ view.addOnLayoutChangeListener(this)
+ }
+
+ override fun onDestroyView() {
+ ApplicationDependencies.getDatabaseObserver().unregisterObserver(this)
+ requireView().removeOnLayoutChangeListener(this)
+ super.onDestroyView()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
- viewModel = ViewModelProviders.of(requireActivity()).get(StickerKeyboardPageViewModel::class.java)
+ viewModel = ViewModelProviders.of(requireActivity(), StickerKeyboardPageViewModel.Factory(requireContext()))
+ .get(StickerKeyboardPageViewModel::class.java)
- tabAdapter = MediaKeyboardBottomTabAdapter(GlideApp.with(this), this::onTabSelected)
- stickerPacksRecycler.adapter = tabAdapter
+ viewModel.stickers.observe(viewLifecycleOwner, keyboardStickerListAdapter::submitList)
+ viewModel.packs.observe(viewLifecycleOwner, stickerPacksAdapter::submitList)
+ viewModel.getSelectedPack().observe(viewLifecycleOwner, this::updateCategoryTab)
- provider = StickerKeyboardProvider(requireActivity(), findListener() ?: throw AssertionError("No sticker listener"))
- provider.requestPresentation(presenter, true)
+ viewModel.refreshStickers()
}
- private fun findListener(): StickerEventListener? {
- return parentFragment as? StickerEventListener ?: requireActivity() as? StickerEventListener
+ private fun onTabSelected(stickerPack: StickerPackListAdapter.StickerPack) {
+ scrollTo(stickerPack.packRecord.packId)
+ viewModel.selectPack(stickerPack.packRecord.packId)
}
- private fun onTabSelected(index: Int) {
- stickerPager.currentItem = index
- stickerPacksRecycler.smoothScrollToPosition(index)
- viewModel.selectedTab = index
- }
+ private fun updateCategoryTab(packId: String) {
+ stickerPacksRecycler.post {
+ val index: Int = stickerPacksAdapter.indexOfFirst(StickerPackListAdapter.StickerPack::class.java) { it.packRecord.packId == packId }
- private inner class StickerPresenter : MediaKeyboardProvider.Presenter {
- override fun present(
- provider: MediaKeyboardProvider,
- pagerAdapter: PagerAdapter,
- iconProvider: MediaKeyboardProvider.TabIconProvider,
- backspaceObserver: MediaKeyboardProvider.BackspaceObserver?,
- addObserver: MediaKeyboardProvider.AddObserver?,
- searchObserver: MediaKeyboardProvider.SearchObserver?,
- startingIndex: Int
- ) {
- if (stickerPager.adapter != pagerAdapter) {
- stickerPager.adapter = pagerAdapter
+ if (index != -1) {
+ stickerPacksRecycler.smoothScrollToPosition(index)
}
- stickerPager.currentItem = viewModel.selectedTab
+ }
+ }
- stickerPager.clearOnPageChangeListeners()
- stickerPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
- override fun onPageSelected(position: Int) {
- tabAdapter.setActivePosition(position)
- stickerPacksRecycler.smoothScrollToPosition(position)
- provider.setCurrentPosition(position)
+ private fun scrollTo(packId: String) {
+ val index = keyboardStickerListAdapter.indexOfFirst(KeyboardStickerListAdapter.StickerHeader::class.java) { it.packId == packId }
+ if (index != -1) {
+ appBarLayout.setExpanded(false, true)
+ packIdSelectionOnScroll.startAutoScrolling()
+ smoothScrollToPositionTop(index)
+ }
+ }
+
+ private fun smoothScrollToPositionTop(position: Int) {
+ val currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
+ val shortTrip = abs(currentPosition - position) < 40
+ if (shortTrip) {
+ val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) {
+ override fun getVerticalSnapPreference(): Int {
+ return SNAP_TO_START
}
+ }
+ smoothScroller.targetPosition = position
+ layoutManager.startSmoothScroll(smoothScroller)
+ } else {
+ layoutManager.scrollToPositionWithOffset(position, 0)
+ }
+ }
- override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
- override fun onPageScrollStateChanged(state: Int) = Unit
- })
+ override fun onStickerClicked(sticker: KeyboardStickerListAdapter.Sticker) {
+ stickerThrottler.publish { findListener()?.onStickerSelected(sticker.stickerRecord) }
+ }
- tabAdapter.setTabIconProvider(iconProvider, pagerAdapter.count)
- tabAdapter.setActivePosition(stickerPager.currentItem)
+ override fun onStickerLongClicked(sticker: KeyboardStickerListAdapter.Sticker) {
+ listTouchListener.enterHoverMode(stickerList, sticker)
+ }
- manageStickers.setOnClickListener { addObserver?.onAddClicked() }
+ override fun getStickerDataFromView(view: View): Pair? {
+ val position: Int = stickerList.getChildAdapterPosition(view)
+ val model: Optional> = keyboardStickerListAdapter.getModel(position)
+ if (model.isPresent && model.get() is KeyboardStickerListAdapter.Sticker) {
+ val sticker = model.get() as KeyboardStickerListAdapter.Sticker
+ return Pair(sticker.uri, sticker.stickerRecord.emoji)
}
- override fun getCurrentPosition(): Int {
- return stickerPager.currentItem
+ return null
+ }
+
+ override fun onStickerPopupStarted() = Unit
+ override fun onStickerPopupEnded() = Unit
+
+ override fun onChanged() {
+ observerThrottler.publish(viewModel::refreshStickers)
+ }
+
+ override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
+ onScreenWidthChanged(view?.width ?: 0)
+ }
+
+ private fun onScreenWidthChanged(@Px newWidth: Int) {
+ layoutManager.spanCount = calculateColumnCount(newWidth)
+ }
+
+ private fun calculateColumnCount(@Px screenWidth: Int): Int {
+ val modifier = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_padding).toFloat()
+ val divisor = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_divisor).toFloat()
+ return ((screenWidth - modifier) / divisor).toInt()
+ }
+
+ private inner class UpdatePackSelectionOnScroll : RecyclerView.OnScrollListener() {
+
+ private var doneScrolling: Boolean = true
+
+ fun startAutoScrolling() {
+ doneScrolling = false
}
- override fun requestDismissal() = Unit
- override fun isVisible(): Boolean = true
+ @Override
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE && !doneScrolling) {
+ doneScrolling = true
+ onScrolled(recyclerView, 0, 0)
+ }
+ }
+
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ if (recyclerView.layoutManager == null || !doneScrolling) {
+ return
+ }
+
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ val index = layoutManager.findFirstCompletelyVisibleItemPosition()
+ val item: Optional> = keyboardStickerListAdapter.getModel(index)
+ if (item.isPresent && item.get() is KeyboardStickerListAdapter.HasPackId) {
+ viewModel.selectPack((item.get() as KeyboardStickerListAdapter.HasPackId).packId)
+ }
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt
index d2b4e25d9..2f3057627 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt
@@ -1,7 +1,66 @@
package org.thoughtcrime.securesms.keyboard.sticker
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.keyboard.sticker.StickerPackListAdapter.StickerPack
+import org.thoughtcrime.securesms.util.MappingModelList
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
-class StickerKeyboardPageViewModel : ViewModel() {
- var selectedTab: Int = 0
+private const val NO_SELECTED_PAGE = "no_selected_page"
+
+class StickerKeyboardPageViewModel(private val repository: StickerKeyboardRepository) : ViewModel() {
+ private val keyboardStickerPacks: MutableLiveData> = MutableLiveData()
+
+ private val selectedPack: MutableLiveData = MutableLiveData(NO_SELECTED_PAGE)
+
+ val packs: LiveData>
+ val stickers: LiveData
+
+ init {
+ val stickerPacks: LiveData> = LiveDataUtil.mapAsync(keyboardStickerPacks) { packs ->
+ packs.map { StickerPack(it) }
+ }
+
+ packs = LiveDataUtil.combineLatest(selectedPack, stickerPacks) { selected, packs ->
+ if (packs.isEmpty()) {
+ packs
+ } else {
+ val actualSelected = if (selected == NO_SELECTED_PAGE) packs[0].packRecord.packId else selected
+ packs.map { it.copy(selected = it.packRecord.packId == actualSelected) }
+ }
+ }
+
+ stickers = LiveDataUtil.mapAsync(keyboardStickerPacks) { packs ->
+ val list = MappingModelList()
+
+ packs.forEach { pack ->
+ list += KeyboardStickerListAdapter.StickerHeader(pack.packId, pack.title, pack.titleResource)
+ list += pack.stickers.map { KeyboardStickerListAdapter.Sticker(pack.packId, it) }
+ }
+
+ list
+ }
+ }
+
+ fun getSelectedPack(): LiveData = selectedPack
+
+ fun selectPack(packId: String) {
+ selectedPack.value = packId
+ }
+
+ fun refreshStickers() {
+ repository.getStickerPacks { keyboardStickerPacks.postValue(it) }
+ }
+
+ class Factory(context: Context) : ViewModelProvider.Factory {
+ private val repository = StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(context))
+
+ override fun create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(StickerKeyboardPageViewModel(repository)))
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardRepository.kt
new file mode 100644
index 000000000..a61e3fe01
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardRepository.kt
@@ -0,0 +1,80 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.net.Uri
+import org.signal.core.util.concurrent.SignalExecutors
+import org.thoughtcrime.securesms.R
+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 java.util.function.Consumer
+
+private const val RECENT_LIMIT = 24
+private const val RECENT_PACK_ID = "RECENT"
+
+class StickerKeyboardRepository(private val stickerDatabase: StickerDatabase) {
+ fun getStickerPacks(consumer: Consumer>) {
+ SignalExecutors.BOUNDED.execute {
+ val packs: MutableList = mutableListOf()
+
+ StickerPackRecordReader(stickerDatabase.installedStickerPacks).use { reader ->
+ var pack: StickerPackRecord? = reader.next
+ while (pack != null) {
+ packs += KeyboardStickerPack(packId = pack.packId, title = pack.title.orNull(), coverUri = pack.cover.uri)
+ pack = reader.next
+ }
+ }
+
+ val fullPacks: MutableList = packs.map { p ->
+ val stickers: MutableList = mutableListOf()
+
+ StickerRecordReader(stickerDatabase.getStickersForPack(p.packId)).use { reader ->
+ var sticker: StickerRecord? = reader.next
+ while (sticker != null) {
+ stickers.add(sticker)
+ sticker = reader.next
+ }
+ }
+
+ p.copy(stickers = stickers)
+ }.toMutableList()
+
+ val recentStickerPack: KeyboardStickerPack = getRecentStickerPack()
+ if (recentStickerPack.stickers.isNotEmpty()) {
+ fullPacks.add(0, recentStickerPack)
+ }
+ consumer.accept(fullPacks)
+ }
+ }
+
+ private fun getRecentStickerPack(): KeyboardStickerPack {
+ val recentStickers: MutableList = mutableListOf()
+
+ StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT)).use { reader ->
+ var recentSticker: StickerRecord? = reader.next
+ while (recentSticker != null) {
+ recentStickers.add(recentSticker)
+ recentSticker = reader.next
+ }
+ }
+
+ return KeyboardStickerPack(
+ packId = RECENT_PACK_ID,
+ title = null,
+ titleResource = R.string.StickerKeyboard__recently_used,
+ coverUri = null,
+ coverResource = R.drawable.ic_recent_20,
+ stickers = recentStickers
+ )
+ }
+
+ data class KeyboardStickerPack(
+ val packId: String,
+ val title: String?,
+ val titleResource: Int? = 0,
+ val coverUri: Uri?,
+ val coverResource: Int? = null,
+ val stickers: List = emptyList()
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerPackListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerPackListAdapter.kt
new file mode 100644
index 000000000..9e6e12175
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerPackListAdapter.kt
@@ -0,0 +1,60 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.ImageView
+import androidx.core.widget.ImageViewCompat
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.glide.cache.ApngOptions
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
+import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingModel
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+class StickerPackListAdapter(private val glideRequests: GlideRequests, private val allowApngAnimation: Boolean, private val onTabSelected: (StickerPack) -> Unit) : MappingAdapter() {
+
+ init {
+ registerFactory(StickerPack::class.java, LayoutFactory(::StickerPackViewHolder, R.layout.keyboard_pager_category_icon))
+ }
+
+ data class StickerPack(val packRecord: StickerKeyboardRepository.KeyboardStickerPack, val selected: Boolean = false) : MappingModel {
+ val loadImage: Boolean = packRecord.coverResource == null
+ val uri: DecryptableUri? = packRecord.coverUri?.let { DecryptableUri(packRecord.coverUri) }
+ val iconResource: Int = packRecord.coverResource ?: 0
+
+ override fun areItemsTheSame(newItem: StickerPack): Boolean {
+ return packRecord.packId == newItem.packRecord.packId
+ }
+
+ override fun areContentsTheSame(newItem: StickerPack): Boolean {
+ return areItemsTheSame(newItem) && selected == newItem.selected
+ }
+ }
+
+ private inner class StickerPackViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val selected: View = findViewById(R.id.category_icon_selected)
+ private val icon: ImageView = findViewById(R.id.category_icon)
+ private val defaultTint: ColorStateList? = ImageViewCompat.getImageTintList(icon)
+
+ override fun bind(model: StickerPack) {
+ itemView.setOnClickListener { onTabSelected(model) }
+
+ selected.isSelected = model.selected
+
+ if (model.loadImage) {
+ ImageViewCompat.setImageTintList(icon, null)
+ icon.alpha = if (model.selected) 1f else 0.5f
+ glideRequests.load(model.uri)
+ .set(ApngOptions.ANIMATE, allowApngAnimation)
+ .into(icon)
+ } else {
+ ImageViewCompat.setImageTintList(icon, defaultTint)
+ icon.setImageResource(model.iconResource)
+ icon.alpha = 1f
+ icon.isSelected = model.selected
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java
index 15bd613be..0c302d8a1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java
@@ -10,6 +10,7 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.keyboard.sticker.KeyboardStickerListAdapter;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.util.Pair;
@@ -24,10 +25,10 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
private WeakReference currentView;
private boolean hoverMode;
- StickerRolloverTouchListener(@NonNull Context context,
- @NonNull GlideRequests glideRequests,
- @NonNull RolloverEventListener eventListener,
- @NonNull RolloverStickerRetriever stickerRetriever)
+ public StickerRolloverTouchListener(@NonNull Context context,
+ @NonNull GlideRequests glideRequests,
+ @NonNull RolloverEventListener eventListener,
+ @NonNull RolloverStickerRetriever stickerRetriever)
{
this.eventListener = eventListener;
this.stickerRetriever = stickerRetriever;
@@ -57,7 +58,7 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
View child = recyclerView.getChildAt(i);
if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) &&
- ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()) &&
+ ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()) &&
child != currentView.get())
{
showStickerForView(recyclerView, child);
@@ -72,25 +73,35 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
public void onRequestDisallowInterceptTouchEvent(boolean b) {
}
- void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
+ public void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
this.hoverMode = true;
showStickerForView(recyclerView, targetView);
}
+ public void enterHoverMode(@NonNull RecyclerView recyclerView, @NonNull KeyboardStickerListAdapter.Sticker sticker) {
+ this.hoverMode = true;
+ showSticker(recyclerView, sticker.getUri(), sticker.getStickerRecord().getEmoji());
+ }
+
private void showStickerForView(@NonNull RecyclerView recyclerView, @NonNull View view) {
Pair