Commit 30b52d33 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Save Emoji data offline on sqlite using Room

parent 2668032b
...@@ -137,11 +137,12 @@ class MainPresenter @Inject constructor( ...@@ -137,11 +137,12 @@ class MainPresenter @Inject constructor(
category = EmojiCategory.CUSTOM.name, category = EmojiCategory.CUSTOM.name,
url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}", url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}",
count = 0, count = 0,
fitzpatrick = Fitzpatrick.Default, fitzpatrick = Fitzpatrick.Default.type,
keywords = customEmoji.aliases, keywords = customEmoji.aliases,
shortnameAlternates = customEmoji.aliases, shortnameAlternates = customEmoji.aliases,
siblings = mutableListOf(), siblings = mutableListOf(),
unicode = "" unicode = "",
default = true
)) ))
} }
......
...@@ -37,6 +37,8 @@ dependencies { ...@@ -37,6 +37,8 @@ dependencies {
implementation libraries.material implementation libraries.material
implementation libraries.glide implementation libraries.glide
kapt libraries.glideProcessor kapt libraries.glideProcessor
implementation libraries.room
kapt libraries.roomProcessor
} }
kotlin { kotlin {
......
package chat.rocket.android.emoji package chat.rocket.android.emoji
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class Emoji( data class Emoji(
val shortname: String, @PrimaryKey
val shortnameAlternates: List<String>, var shortname: String = "",
val unicode: String, var shortnameAlternates: List<String> = listOf(),
val keywords: List<String>, var unicode: String = "",
val category: String, @Ignore val keywords: List<String> = listOf(),
val count: Int = 0, var category: String = "",
val siblings: MutableCollection<Emoji> = mutableListOf(), var count: Int = 0,
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default, var siblings: List<String> = listOf(),
val url: String? = null // Filled for custom emojis var fitzpatrick: String = Fitzpatrick.Default.type,
var url: String? = null, // Filled for custom emojis
var default: Boolean = true
) )
package chat.rocket.android.emoji
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.IGNORE
import androidx.room.Query
import androidx.room.Update
@Dao
interface EmojiDao {
@Query("SELECT * FROM emoji")
fun loadAllEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE url IS NULL")
fun loadSimpleEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE url IS NOT NULL")
fun loadAllCustomEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE shortname=:shortname")
fun loadEmojiByShortname(shortname: String): Emoji?
@Query("SELECT * FROM emoji WHERE UPPER(category)=UPPER(:category)")
fun loadEmojisByCategory(category: String): List<Emoji>
@Insert(onConflict = IGNORE)
fun insertEmoji(emoji: Emoji)
@Insert(onConflict = IGNORE)
fun insertAllEmojis(vararg emojis: Emoji)
@Update
fun updateEmoji(emoji: Emoji)
@Delete
fun deleteEmoji(emoji: Emoji)
@Query("DELETE FROM emoji")
fun deleteAll()
}
...@@ -22,6 +22,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory ...@@ -22,6 +22,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) { class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
...@@ -49,8 +51,10 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -49,8 +51,10 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} }
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
setupViewPager() launch(UI) {
setupBottomBar() setupViewPager()
setupBottomBar()
}
} }
private fun setupBottomBar() { private fun setupBottomBar() {
...@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} }
} }
private fun setupViewPager() { private suspend fun setupViewPager() {
context.let { context.let {
val callback = when (it) { val callback = when (it) {
is EmojiKeyboardListener -> it is EmojiKeyboardListener -> it
...@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
} }
}) })
viewPager.offscreenPageLimit = EmojiCategory.values().size viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter viewPager.adapter = adapter
...@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} else { } else {
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
} }
viewPager.currentItem = currentTab viewPager.currentItem = currentTab
} }
} }
......
...@@ -2,19 +2,14 @@ package chat.rocket.android.emoji ...@@ -2,19 +2,14 @@ package chat.rocket.android.emoji
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.style.ImageSpan import android.text.style.ImageSpan
import android.util.Log import android.util.Log
import chat.rocket.android.emoji.internal.GlideApp import chat.rocket.android.emoji.internal.GlideApp
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.gif.GifDrawable import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestFutureTarget
import com.bumptech.glide.request.RequestOptions
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Deferred import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.async
......
...@@ -14,6 +14,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory ...@@ -14,6 +14,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import kotlinx.android.synthetic.main.emoji_picker.* import kotlinx.android.synthetic.main.emoji_picker.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiPickerPopup(context: Context) : Dialog(context) { class EmojiPickerPopup(context: Context) : Dialog(context) {
...@@ -27,8 +29,10 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -27,8 +29,10 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
setContentView(R.layout.emoji_picker) setContentView(R.layout.emoji_picker)
tabs.setupWithViewPager(pager_categories) tabs.setupWithViewPager(pager_categories)
setupViewPager() launch(UI) {
setSize() setupViewPager()
setSize()
}
} }
private fun setSize() { private fun setSize() {
...@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
window.setLayout(dialogWidth, dialogHeight) window.setLayout(dialogWidth, dialogHeight)
} }
private fun setupViewPager() { private suspend fun setupViewPager() {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener { adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji) EmojiRepository.addToRecents(emoji)
......
...@@ -5,9 +5,11 @@ import android.content.SharedPreferences ...@@ -5,9 +5,11 @@ import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import chat.rocket.android.emoji.internal.EmojiCategory import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import chat.rocket.android.emoji.internal.db.EmojiDatabase
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader import java.io.BufferedReader
...@@ -24,18 +26,20 @@ object EmojiRepository { ...@@ -24,18 +26,20 @@ object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE) private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
private val shortNameToUnicode = HashMap<String, String>() private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):") private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>() private var customEmojis = listOf<Emoji>()
private var customEmojis: List<Emoji> = emptyList()
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface internal lateinit var cachedTypeface: Typeface
private lateinit var db: EmojiDatabase
fun load(context: Context, customEmojis: List<Emoji> = emptyList(), path: String = "emoji.json") { fun load(context: Context, customEmojis: List<Emoji> = emptyList(), path: String = "emoji.json") {
launch(CommonPool) { launch(CommonPool) {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
this@EmojiRepository.customEmojis = customEmojis this@EmojiRepository.customEmojis = customEmojis
val allEmojis = mutableListOf<Emoji>()
db = EmojiDatabase.getInstance(context)
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE) preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear()
val stream = context.assets.open(path) val stream = context.assets.open(path)
// Load emojis from emojione ttf file temporarily here. We still need to work on them.
val emojis = loadEmojis(stream).also { val emojis = loadEmojis(stream).also {
it.addAll(customEmojis) it.addAll(customEmojis)
}.toList() }.toList()
...@@ -43,9 +47,11 @@ object EmojiRepository { ...@@ -43,9 +47,11 @@ object EmojiRepository {
for (emoji in emojis) { for (emoji in emojis) {
val unicodeIntList = mutableListOf<Int>() val unicodeIntList = mutableListOf<Int>()
// If empty it's a custom emoji. emoji.category = emoji.category.toUpperCase()
// If unicode is empty it's a custom emoji, just add it.
if (emoji.unicode.isEmpty()) { if (emoji.unicode.isEmpty()) {
ALL_EMOJIS.add(emoji) allEmojis.add(emoji)
continue continue
} }
...@@ -59,31 +65,38 @@ object EmojiRepository { ...@@ -59,31 +65,38 @@ object EmojiRepository {
unicodeIntList.add(value) unicodeIntList.add(value)
} }
} }
val unicodeIntArray = unicodeIntList.toIntArray() val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size) val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
val emojiWithUnicode = emoji.copy(unicode = unicode) val emojiWithUnicode = emoji.copy(unicode = unicode)
if (hasFitzpatrick(emoji.shortname)) { if (hasFitzpatrick(emoji.shortname)) {
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname) val matchResult = FITZPATRICK_REGEX.find(emoji.shortname)
val prefix = matchResult!!.groupValues[1] + ":" val prefix = matchResult!!.groupValues[1] + ":"
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2]) val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2])
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix } val defaultEmoji = allEmojis.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick) val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick.type)
if (defaultEmoji != null) { if (defaultEmoji != null) {
defaultEmoji.siblings.add(emojiWithFitzpatrick) emojiWithFitzpatrick.default = false
defaultEmoji.siblings.toMutableList().add(emoji.shortname)
} else { } else {
// This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1: // This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1:
// In this case, the default emoji becomes the first toned one. // In this case, the default emoji becomes the first toned one.
ALL_EMOJIS.add(emojiWithFitzpatrick) allEmojis.add(emojiWithFitzpatrick)
} }
} else { } else {
ALL_EMOJIS.add(emojiWithUnicode) allEmojis.add(emojiWithUnicode)
} }
shortNameToUnicode.apply { shortNameToUnicode.apply {
put(emoji.shortname, unicode) put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) } emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
} }
} }
saveEmojisToDatabase(allEmojis.toList())
val density = context.resources.displayMetrics.density val density = context.resources.displayMetrics.density
val px = (32 * density).toInt() val px = (32 * density).toInt()
...@@ -96,6 +109,12 @@ object EmojiRepository { ...@@ -96,6 +109,12 @@ object EmojiRepository {
} }
} }
private suspend fun saveEmojisToDatabase(emojis: List<Emoji>) {
withContext(CommonPool) {
db.emojiDao().insertAllEmojis(*emojis.toTypedArray())
}
}
private fun hasFitzpatrick(shortname: String): Boolean { private fun hasFitzpatrick(shortname: String): Boolean {
return FITZPATRICK_REGEX matches shortname return FITZPATRICK_REGEX matches shortname
} }
...@@ -105,21 +124,15 @@ object EmojiRepository { ...@@ -105,21 +124,15 @@ object EmojiRepository {
* *
* @return All emojis for all categories. * @return All emojis for all categories.
*/ */
internal fun getAll() = ALL_EMOJIS internal suspend fun getAll(): List<Emoji> = withContext(CommonPool) {
return@withContext db.emojiDao().loadAllEmojis()
/**
* Get all emojis for a given category.
*
* @param category Emoji category such as: PEOPLE, NATURE, ETC
*
* @return All emoji from specified category
*/
internal fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
} }
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> { internal suspend fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() } val list = withContext(CommonPool) {
db.emojiDao().loadEmojisByCategory(category.name.toLowerCase())
}
return buildSequence { return buildSequence {
list.forEach { list.forEach {
yield(it) yield(it)
...@@ -134,7 +147,9 @@ object EmojiRepository { ...@@ -134,7 +147,9 @@ object EmojiRepository {
* *
* @return Emoji given by shortname or null * @return Emoji given by shortname or null
*/ */
internal fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname } private suspend fun getEmojiByShortname(shortname: String): Emoji? = withContext(CommonPool) {
return@withContext db.emojiDao().loadEmojiByShortname(shortname)
}
/** /**
* Add an emoji to the Recents category. * Add an emoji to the Recents category.
...@@ -151,6 +166,14 @@ object EmojiRepository { ...@@ -151,6 +166,14 @@ object EmojiRepository {
preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply() preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
} }
internal suspend fun getCustomEmojisAsync(): List<Emoji> {
return withContext(CommonPool) {
db.emojiDao().loadAllCustomEmojis().also {
this.customEmojis = it
}
}
}
internal fun getCustomEmojis(): List<Emoji> = customEmojis internal fun getCustomEmojis(): List<Emoji> = customEmojis
/** /**
...@@ -158,19 +181,22 @@ object EmojiRepository { ...@@ -158,19 +181,22 @@ object EmojiRepository {
* *
* @return All recent emojis ordered by usage. * @return All recent emojis ordered by usage.
*/ */
internal fun getRecents(): List<Emoji> { internal suspend fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>() val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}")) val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname) // for (shortname in recentsJson.keys()) {
emoji?.let { // val emoji = getEmojiByShortname(shortname)
val useCount = recentsJson.getInt(it.shortname) // emoji?.let {
list.add(it.copy(count = useCount)) // val useCount = recentsJson.getInt(it.shortname)
} // list.add(it.copy(count = useCount))
} // }
list.sortWith(Comparator { o1, o2 -> // }
o2.count - o1.count //
}) // list.sortWith(Comparator { o1, o2 ->
// o2.count - o1.count
// })
return list return list
} }
......
...@@ -53,17 +53,21 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -53,17 +53,21 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
} else { } else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray()) sequenceOf(*EmojiRepository.getRecents().toTypedArray())
} }
val recentEmojiSize = EmojiRepository.getRecents().size val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0 text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) { if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener) val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter emoji_recycler_view.adapter = adapter
adapters[category] = adapter adapters[category] = adapter
adapter.addEmojisFromSequence(emojis) adapter.addEmojisFromSequence(emojis)
} }
adapters[category]!!.setFitzpatrick(fitzpatrick) adapters[category]!!.setFitzpatrick(fitzpatrick)
} }
} }
return view return view
} }
...@@ -126,7 +130,13 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -126,7 +130,13 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) { override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
val emoji = emojis[position] val emoji = emojis[position]
holder.bind( holder.bind(
emoji.siblings.find { it.fitzpatrick == fitzpatrick } ?: emoji emoji.siblings.find {
it.endsWith(fitzpatrick.type)
}?.let { shortname ->
emojis.firstOrNull {
it.shortname == shortname
}
} ?: emoji
) )
} }
......
package chat.rocket.android.emoji.internal.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiDao
@Database(entities = [Emoji::class], version = 1)
@TypeConverters(StringListConverter::class)
abstract class EmojiDatabase : RoomDatabase() {
abstract fun emojiDao(): EmojiDao
companion object : SingletonHolder<EmojiDatabase, Context>({
Room.databaseBuilder(it.applicationContext,
EmojiDatabase::class.java, "emoji.db")
.fallbackToDestructiveMigration()
.build()
})
}
open class SingletonHolder<out T, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@kotlin.jvm.Volatile
private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
package chat.rocket.android.emoji.internal.db
import androidx.room.TypeConverter
internal object StringListConverter {
@TypeConverter
@JvmStatic
fun toString(list: List<String>?): String? {
return if (list == null) null else list.joinToString(separator = ",")
}
@TypeConverter
@JvmStatic
fun toStringList(value: String?): List<String>? {
return value?.split(",")
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment