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(
category = EmojiCategory.CUSTOM.name,
url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}",
count = 0,
fitzpatrick = Fitzpatrick.Default,
fitzpatrick = Fitzpatrick.Default.type,
keywords = customEmoji.aliases,
shortnameAlternates = customEmoji.aliases,
siblings = mutableListOf(),
unicode = ""
unicode = "",
default = true
))
}
......
......@@ -37,6 +37,8 @@ dependencies {
implementation libraries.material
implementation libraries.glide
kapt libraries.glideProcessor
implementation libraries.room
kapt libraries.roomProcessor
}
kotlin {
......
package chat.rocket.android.emoji
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class Emoji(
val shortname: String,
val shortnameAlternates: List<String>,
val unicode: String,
val keywords: List<String>,
val category: String,
val count: Int = 0,
val siblings: MutableCollection<Emoji> = mutableListOf(),
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default,
val url: String? = null // Filled for custom emojis
@PrimaryKey
var shortname: String = "",
var shortnameAlternates: List<String> = listOf(),
var unicode: String = "",
@Ignore val keywords: List<String> = listOf(),
var category: String = "",
var count: Int = 0,
var siblings: List<String> = listOf(),
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
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
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) {
......@@ -49,9 +51,11 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
}
override fun onViewCreated(view: View) {
launch(UI) {
setupViewPager()
setupBottomBar()
}
}
private fun setupBottomBar() {
searchView.setOnClickListener {
......@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
}
}
private fun setupViewPager() {
private suspend fun setupViewPager() {
context.let {
val callback = when (it) {
is EmojiKeyboardListener -> it
......@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji)
}
})
viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter
......@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} else {
EmojiCategory.RECENTS.ordinal
}
viewPager.currentItem = currentTab
}
}
......
......@@ -2,19 +2,14 @@ package chat.rocket.android.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ImageSpan
import android.util.Log
import chat.rocket.android.emoji.internal.GlideApp
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
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.Deferred
import kotlinx.coroutines.experimental.async
......
......@@ -14,6 +14,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import kotlinx.android.synthetic.main.emoji_picker.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiPickerPopup(context: Context) : Dialog(context) {
......@@ -27,9 +29,11 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
setContentView(R.layout.emoji_picker)
tabs.setupWithViewPager(pager_categories)
launch(UI) {
setupViewPager()
setSize()
}
}
private fun setSize() {
val lp = WindowManager.LayoutParams()
......@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
window.setLayout(dialogWidth, dialogHeight)
}
private fun setupViewPager() {
private suspend fun setupViewPager() {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
......
......@@ -5,9 +5,11 @@ import android.content.SharedPreferences
import android.graphics.Typeface
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import chat.rocket.android.emoji.internal.db.EmojiDatabase
import com.bumptech.glide.Glide
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
......@@ -24,18 +26,20 @@ object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>()
private var customEmojis: List<Emoji> = emptyList()
private var customEmojis = listOf<Emoji>()
private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface
private lateinit var db: EmojiDatabase
fun load(context: Context, customEmojis: List<Emoji> = emptyList(), path: String = "emoji.json") {
launch(CommonPool) {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
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)
ALL_EMOJIS.clear()
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 {
it.addAll(customEmojis)
}.toList()
......@@ -43,9 +47,11 @@ object EmojiRepository {
for (emoji in emojis) {
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()) {
ALL_EMOJIS.add(emoji)
allEmojis.add(emoji)
continue
}
......@@ -59,31 +65,38 @@ object EmojiRepository {
unicodeIntList.add(value)
}
}
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
val emojiWithUnicode = emoji.copy(unicode = unicode)
if (hasFitzpatrick(emoji.shortname)) {
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname)
val prefix = matchResult!!.groupValues[1] + ":"
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2])
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick)
val defaultEmoji = allEmojis.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick.type)
if (defaultEmoji != null) {
defaultEmoji.siblings.add(emojiWithFitzpatrick)
emojiWithFitzpatrick.default = false
defaultEmoji.siblings.toMutableList().add(emoji.shortname)
} else {
// 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.
ALL_EMOJIS.add(emojiWithFitzpatrick)
allEmojis.add(emojiWithFitzpatrick)
}
} else {
ALL_EMOJIS.add(emojiWithUnicode)
allEmojis.add(emojiWithUnicode)
}
shortNameToUnicode.apply {
put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
}
}
saveEmojisToDatabase(allEmojis.toList())
val density = context.resources.displayMetrics.density
val px = (32 * density).toInt()
......@@ -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 {
return FITZPATRICK_REGEX matches shortname
}
......@@ -105,21 +124,15 @@ object EmojiRepository {
*
* @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 suspend fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = withContext(CommonPool) {
db.emojiDao().loadEmojisByCategory(category.name.toLowerCase())
}
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
return buildSequence {
list.forEach {
yield(it)
......@@ -134,7 +147,9 @@ object EmojiRepository {
*
* @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.
......@@ -151,6 +166,14 @@ object EmojiRepository {
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
/**
......@@ -158,19 +181,22 @@ object EmojiRepository {
*
* @return All recent emojis ordered by usage.
*/
internal fun getRecents(): List<Emoji> {
internal suspend fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname)
emoji?.let {
val useCount = recentsJson.getInt(it.shortname)
list.add(it.copy(count = useCount))
}
}
list.sortWith(Comparator { o1, o2 ->
o2.count - o1.count
})
// for (shortname in recentsJson.keys()) {
// val emoji = getEmojiByShortname(shortname)
// emoji?.let {
// val useCount = recentsJson.getInt(it.shortname)
// list.add(it.copy(count = useCount))
// }
// }
//
// list.sortWith(Comparator { o1, o2 ->
// o2.count - o1.count
// })
return list
}
......
......@@ -53,17 +53,21 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
} else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray())
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter
adapters[category] = adapter
adapter.addEmojisFromSequence(emojis)
}
adapters[category]!!.setFitzpatrick(fitzpatrick)
}
}
return view
}
......@@ -126,7 +130,13 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
val emoji = emojis[position]
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