From d8ac11d193b48c8ef5949436572c4d26fb056302 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 17 Feb 2024 22:43:15 +0530 Subject: [PATCH] Add support to store history and favourite words. (#63) * Add support to store history and favourite words. * Add support for deleting from history and favourite page. * Remove copy in favour of favourite since Android only allows 3 actions. Update db download URL. --- app/build.gradle | 9 +- app/src/main/AndroidManifest.xml | 32 +++-- .../FavouriteActivity.kt | 48 ++++++++ .../notificationdictionary/HistoryActivity.kt | 48 ++++++++ .../notificationdictionary/MainActivity.kt | 30 +++-- .../ProcessTextActivity.kt | 108 +++++++++------- .../xtreak/notificationdictionary/Utils.kt | 34 ++++- .../adapters/HistoryAdapter.kt | 116 ++++++++++++++++++ .../daos/DictionaryDao.kt | 6 +- .../notificationdictionary/daos/HistoryDao.kt | 53 ++++++++ .../database/AppDatabase.kt | 15 ++- .../notificationdictionary/entities/Word.kt | 12 +- .../main/res/layout/activity_favourite.xml | 29 +++++ app/src/main/res/layout/activity_history.xml | 29 +++++ app/src/main/res/layout/history_layout.xml | 72 +++++++++++ app/src/main/res/menu/menu.xml | 9 ++ app/src/main/res/values/strings.xml | 3 + 17 files changed, 580 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/com/xtreak/notificationdictionary/FavouriteActivity.kt create mode 100644 app/src/main/java/com/xtreak/notificationdictionary/HistoryActivity.kt create mode 100644 app/src/main/java/com/xtreak/notificationdictionary/adapters/HistoryAdapter.kt create mode 100644 app/src/main/java/com/xtreak/notificationdictionary/daos/HistoryDao.kt create mode 100644 app/src/main/res/layout/activity_favourite.xml create mode 100644 app/src/main/res/layout/activity_history.xml create mode 100644 app/src/main/res/layout/history_layout.xml diff --git a/app/build.gradle b/app/build.gradle index 7b075bf..13b9870 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,8 +28,8 @@ android { applicationId "com.xtreak.notificationdictionary" minSdk 24 targetSdk 33 - versionCode 22 - versionName "0.0.22" + versionCode 23 + versionName "0.0.23" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -48,6 +48,9 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + viewBinding true + } } dependencies { @@ -68,7 +71,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - def room_version = "2.3.0" + def room_version = "2.5.0" apply plugin: 'kotlin-kapt' implementation "androidx.room:room-runtime:$room_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5321979..06c4b95 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.xtreak.notificationdictionary"> @@ -11,7 +11,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.NotificationDictionary" > + android:theme="@style/Theme.NotificationDictionary"> + + + + android:parentActivityName=".MainActivity"> + + + + - + android:parentActivityName=".MainActivity"> + + - + android:exported="true"> + @@ -66,4 +82,4 @@ android:theme="@style/Theme.MaterialComponents.NoActionBar" /> - \ No newline at end of file + diff --git a/app/src/main/java/com/xtreak/notificationdictionary/FavouriteActivity.kt b/app/src/main/java/com/xtreak/notificationdictionary/FavouriteActivity.kt new file mode 100644 index 0000000..35f4ee6 --- /dev/null +++ b/app/src/main/java/com/xtreak/notificationdictionary/FavouriteActivity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, Karthikeyan Singaravelan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.xtreak.notificationdictionary + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.xtreak.notificationdictionary.adapters.HistoryAdapter +import java.util.concurrent.Executors + +class FavouriteActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_favourite) + + val executor = Executors.newSingleThreadExecutor() + val handler = Handler(Looper.getMainLooper()) + + val linearLayoutManager = LinearLayoutManager(this) + linearLayoutManager.orientation = LinearLayoutManager.VERTICAL + + executor.execute { + val database = AppDatabase.getDatabase(this) + val historyDao = database.historyDao() + var entries = historyDao.getAllFavouriteEntriesWithMeaning() + + handler.post { + val mRecyclerView = findViewById(R.id.favouriteRecyclerView) + val mListadapter = HistoryAdapter(entries as MutableList, this, true) + mRecyclerView.adapter = mListadapter + mRecyclerView.layoutManager = linearLayoutManager + mListadapter.notifyItemRangeChanged(1, 100) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xtreak/notificationdictionary/HistoryActivity.kt b/app/src/main/java/com/xtreak/notificationdictionary/HistoryActivity.kt new file mode 100644 index 0000000..6a45278 --- /dev/null +++ b/app/src/main/java/com/xtreak/notificationdictionary/HistoryActivity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, Karthikeyan Singaravelan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.xtreak.notificationdictionary + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.xtreak.notificationdictionary.adapters.HistoryAdapter +import java.util.concurrent.Executors + +class HistoryActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_history) + + val executor = Executors.newSingleThreadExecutor() + val handler = Handler(Looper.getMainLooper()) + + val linearLayoutManager = LinearLayoutManager(this) + linearLayoutManager.orientation = LinearLayoutManager.VERTICAL + + executor.execute { + val database = AppDatabase.getDatabase(this) + val historyDao = database.historyDao() + var entries = historyDao.getAllEntriesWithMeaning() + + handler.post { + val mRecyclerView = findViewById(R.id.historyRecyclerView) + val mListadapter = HistoryAdapter(entries as MutableList, this, false) + mRecyclerView.adapter = mListadapter + mRecyclerView.layoutManager = linearLayoutManager + mListadapter.notifyItemRangeChanged(1, 100) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xtreak/notificationdictionary/MainActivity.kt b/app/src/main/java/com/xtreak/notificationdictionary/MainActivity.kt index b30b1f5..a0e246f 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/MainActivity.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/MainActivity.kt @@ -19,6 +19,7 @@ import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import android.os.* +import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View @@ -144,26 +145,26 @@ class MainActivity : AppCompatActivity() { Word( 1, "", - "Read meanings aloud as you read", + "Store history and favourite words", 1, 1, - """Enable Read switch at the right top to read aloud meaning of the word when the notification is created. There is also read button per notification to read meaning for each word.""" + """History of searches is stored. Words can also be starred from notification to be stored as favourite. History and Favourite are accessible from the menu at right top. In case of issues due to update please uninstall and try reinstalling the app since it needs database changes.""" ), Word( 1, "", - "Copy and share", + "Read meanings aloud as you read", 1, 1, - """Click on meaning to copy. Long press to share meaning with others. Notifications also have button for these actions.""" + """Enable Read switch at the right top to read aloud meaning of the word when the notification is created. There is also read button per notification to read meaning for each word.""" ), Word( 1, "", - "Multilingual support", + "Copy and share", 1, 1, - """Languages supported include French, German and Polish.""" + """Click on meaning to copy. Long press to share meaning with others. Notifications also have button for these actions.""" ), Word( 1, @@ -409,6 +410,14 @@ class MainActivity : AppCompatActivity() { .withLicenseShown(true) .start(this) } + R.id.history -> { + val history_activity = Intent(applicationContext, HistoryActivity::class.java) + startActivityForResult(history_activity, 0) + } + R.id.favourite -> { + val favourite_activity = Intent(applicationContext, FavouriteActivity::class.java) + startActivityForResult(favourite_activity, 0) + } } return true } @@ -429,7 +438,7 @@ class MainActivity : AppCompatActivity() { // But we don't want the user to cancel this. It's one time and takes a couple of seconds // TODO: Make this configurable based on environment? - val url = "https://xtreak.sfo3.cdn.digitaloceanspaces.com/dictionaries/$database_name.zip" + val url = "https://xtreak.sfo3.cdn.digitaloceanspaces.com/dictionaries/v2/$database_name.zip" // val url = "http://192.168.0.105:8000/$database_name.zip" // for local mobile testing // val url = "http://10.0.2.2:8000/$database_name.zip" // for local emulator testing @@ -570,12 +579,17 @@ class MainActivity : AppCompatActivity() { executor.execute { val database = AppDatabase.getDatabase(this) val dao = database.dictionaryDao() + val historyDao = database.historyDao() var meanings: List try { meanings = dao.getAllMeaningsByWord(word) + if (meanings.isNotEmpty()) { + addHistoryEntry(historyDao, word) + } } catch (e: Exception) { Sentry.captureException(e) + Log.d("ndict:", e.toString()) meanings = listOf( Word( 1, "", "Error", 1, 1, @@ -606,6 +620,4 @@ class MainActivity : AppCompatActivity() { } } } - - } diff --git a/app/src/main/java/com/xtreak/notificationdictionary/ProcessTextActivity.kt b/app/src/main/java/com/xtreak/notificationdictionary/ProcessTextActivity.kt index 43988f7..58d1bf8 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/ProcessTextActivity.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/ProcessTextActivity.kt @@ -54,25 +54,30 @@ open class ProcessIntentActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val word: String + var lexicalCategory: String = "" val context = applicationContext + val executor = Executors.newSingleThreadExecutor() + var definition = "No meaning found" + if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") { word = intent.getCharSequenceExtra(Intent.EXTRA_TEXT).toString().lowercase() } else { word = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString().lowercase() } - val executor = Executors.newSingleThreadExecutor() - var definition = "No meaning found" // https://stackoverflow.com/questions/1250643/how-to-wait-for-all-threads-to-finish-using-executorservice executor.execute { val database = AppDatabase.getDatabase(this) val dao = database.dictionaryDao() + val historyDao = database.historyDao() + var meaning: Word? try { meaning = dao.getMeaningsByWord(word, 1) if (meaning != null) { resolveRedirectMeaning(listOf(meaning), dao) + addHistoryEntry(historyDao, word) } } catch (e: Exception) { Sentry.captureException(e) @@ -83,7 +88,13 @@ open class ProcessIntentActivity : AppCompatActivity() { "Please turn on your internet connection and restart the app to download the database." ) } - definition = meaning?.definition ?: "No meaning found" + lexicalCategory = meaning?.lexicalCategory ?: "" + + if (meaning?.definition != null) { + definition = "${lexicalCategory}, ${meaning?.definition}" + } else { + definition = "No meaning found" + } } executor.shutdown() executor.awaitTermination(10, TimeUnit.SECONDS) @@ -103,9 +114,13 @@ open class ProcessIntentActivity : AppCompatActivity() { .setTimeoutAfter(NOTIFICATION_TIMEOUT.toLong()) if (definition != "No meaning found") { - addCopyButton(word, definition, context, builder) + /* https://developer.android.com/reference/android/app/Notification.Builder.html#addAction(android.app.Notification.Action) + A notification in its expanded form can display up to 3 actions, from left to right in the order they were added. + */ + addShareButton(word, definition, context, builder) addReadButton(word, definition, context, builder) + addFavouriteButton(word, context, builder) } val intent = Intent( @@ -147,52 +162,12 @@ open class ProcessIntentActivity : AppCompatActivity() { this.finish() } - private fun addCopyButton( - word: String, - definition: String, - context: Context, - builder: NotificationCompat.Builder - ) { - // Ref : https://stackoverflow.com/questions/14291436/copy-to-clipboard-by-notification-action - val notificationCopy: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent?) { - val clipboard: ClipboardManager = - context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("label", "${word} - ${definition}") - clipboard.setPrimaryClip(clip) - - // unregister the receiver else they will keep adding themselves to context resulting in duplicate calls - try { - context.unregisterReceiver(this) - } catch (e: IllegalArgumentException) { - Sentry.captureException(e) - Log.e("Notification Dictionary", "Error in unregistering the receiver") - } - } - } - - val intentFilter = IntentFilter("com.xtreak.notificationdictionary.ACTION_COPY") - context.registerReceiver(notificationCopy, intentFilter) - - val copy = Intent("com.xtreak.notificationdictionary.ACTION_COPY") - val nCopy = - PendingIntent.getBroadcast( - context, - 0, - copy, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - builder.addAction(NotificationCompat.Action(null, "Copy", nCopy)) - } - private fun addShareButton( word: String, definition: String, context: Context, builder: NotificationCompat.Builder ) { - // Ref : https://stackoverflow.com/questions/14291436/copy-to-clipboard-by-notification-action val notificationShare: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { val sharingIntent = Intent(Intent.ACTION_SEND) @@ -206,6 +181,7 @@ open class ProcessIntentActivity : AppCompatActivity() { // unregister the receiver else they will keep adding themselves to context resulting in duplicate calls try { context.unregisterReceiver(this) + Sentry.captureMessage("Process share event.") } catch (e: IllegalArgumentException) { Sentry.captureException(e) Log.e("Notification Dictionary", "Error in unregistering the receiver") @@ -240,7 +216,6 @@ open class ProcessIntentActivity : AppCompatActivity() { context: Context, builder: NotificationCompat.Builder, ) { - // Ref : https://stackoverflow.com/questions/14291436/copy-to-clipboard-by-notification-action val notificationRead: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { TTSOnInitListener(word, definition, context) @@ -270,6 +245,49 @@ open class ProcessIntentActivity : AppCompatActivity() { builder.addAction(NotificationCompat.Action(null, "Read", nRead)) } + private fun addFavouriteButton( + word: String, + context: Context, + builder: NotificationCompat.Builder + ) { + val database = AppDatabase.getDatabase(this) + + val notificationFavourite: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + val executor = Executors.newSingleThreadExecutor() + + // unregister the receiver else they will keep adding themselves to context resulting in duplicate calls + try { + executor.execute { + val historyDao = database.historyDao() + historyDao.addFavourite(word) + } + context.unregisterReceiver(this) + Sentry.captureMessage("Process favourite event.") + } catch (e: Exception) { + Sentry.captureException(e) + } finally { + executor.shutdown() + } + } + } + + val intentFilter = IntentFilter("com.xtreak.notificationdictionary.ACTION_FAVOURITE") + context.registerReceiver(notificationFavourite, intentFilter) + + val star = Intent("com.xtreak.notificationdictionary.ACTION_FAVOURITE") + val nStar = + PendingIntent.getBroadcast( + context, + 0, + star, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + builder.addAction(NotificationCompat.Action(null, "Favourite", nStar)) + } + + } diff --git a/app/src/main/java/com/xtreak/notificationdictionary/Utils.kt b/app/src/main/java/com/xtreak/notificationdictionary/Utils.kt index 428b76d..c7324ff 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/Utils.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/Utils.kt @@ -10,6 +10,9 @@ package com.xtreak.notificationdictionary +import android.util.Log +import io.sentry.Sentry +import java.lang.Exception import java.util.* /** @@ -35,4 +38,33 @@ fun resolveRedirectMeaning( } } } -} \ No newline at end of file +} + +fun addHistoryEntry( + historyDao: HistoryDao, + word: String +) { + + val historyEntry = historyDao.getHistory(word) + + try { + if (historyEntry != null) { + historyDao.updateHistory( + word = word, + lastAccessedAt = System.currentTimeMillis() + ) + } else { + historyDao.insertHistory( + History( + id = null, + word = word, + isFavourite = 0, + lastAccessedAt = System.currentTimeMillis() + ) + ) + } + } catch (e: Exception) { + Sentry.captureException(e) + } + +} diff --git a/app/src/main/java/com/xtreak/notificationdictionary/adapters/HistoryAdapter.kt b/app/src/main/java/com/xtreak/notificationdictionary/adapters/HistoryAdapter.kt new file mode 100644 index 0000000..ecfd335 --- /dev/null +++ b/app/src/main/java/com/xtreak/notificationdictionary/adapters/HistoryAdapter.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024, Karthikeyan Singaravelan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.xtreak.notificationdictionary.adapters + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.text.Html +import android.text.Spanned +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.content.ContextCompat.startActivity +import androidx.recyclerview.widget.RecyclerView +import com.xtreak.notificationdictionary.AppDatabase +import com.xtreak.notificationdictionary.HistoryDao +import com.xtreak.notificationdictionary.MainActivity +import com.xtreak.notificationdictionary.R +import java.util.concurrent.Executors + + +class HistoryAdapter( + data: MutableList, + context: Context, + favourite_page: Boolean +) : + RecyclerView.Adapter() { + private val meaningList: MutableList = data + private val context: Context = context + private val favourite_page: Boolean = favourite_page + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var lexicalCategory: TextView = itemView.findViewById(R.id.lexicalCategoryhistory) + var wordMeaning: TextView = itemView.findViewById(R.id.wordMeaninghistory) + var deleteEntry: ImageButton = itemView.findViewById(R.id.deleteEntryhistory) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.history_layout, parent, false) + + return ViewHolder(view) + } + + private fun formatHtml(content: String): Spanned? { + return Html.fromHtml(content, Html.FROM_HTML_MODE_COMPACT) + } + + override fun getItemCount(): Int { + return meaningList.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (position > meaningList.size) { + return + } + + val word = meaningList[position].word!! + holder.lexicalCategory.text = formatHtml(" ${word}") + holder.wordMeaning.text = formatHtml("${meaningList[position].definition}
") + + val listener = { + val intent = Intent(context, MainActivity::class.java) + intent.putExtra("NotificationWord", word) + startActivity(context, intent, null) + } + + holder.wordMeaning.setOnClickListener { + listener() + } + + holder.lexicalCategory.setOnClickListener { + listener() + } + + holder.deleteEntry.setOnClickListener { + val executor = Executors.newSingleThreadExecutor() + val handler = Handler(Looper.getMainLooper()) + + executor.execute { + + // Reuse such that if it's history page remove the word and if it's favourite then toggle + // favourite column. Maybe refactor this to a method and have a base class to override the + // action instead of a toggle here. + if (!this.favourite_page) { + val database = AppDatabase.getDatabase(context) + val historyDao = database.historyDao() + historyDao.deleteHistory(word) + } else { + val database = AppDatabase.getDatabase(context) + val historyDao = database.historyDao() + historyDao.removeFavourite(word) + } + meaningList.removeAt(position) + + handler.post { + this.notifyDataSetChanged() + } + } + } + } +} diff --git a/app/src/main/java/com/xtreak/notificationdictionary/daos/DictionaryDao.kt b/app/src/main/java/com/xtreak/notificationdictionary/daos/DictionaryDao.kt index 811d022..f3f7be0 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/daos/DictionaryDao.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/daos/DictionaryDao.kt @@ -10,9 +10,7 @@ package com.xtreak.notificationdictionary -import androidx.room.Dao -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.* @Dao interface DictionaryDao { @@ -23,4 +21,4 @@ interface DictionaryDao { @RewriteQueriesToDropUnusedColumns @Query("SELECT CASE WHEN lexical_category LIKE 'Proper noun' THEN 2 ELSE 1 END AS priority, * FROM dictionary WHERE word = :word COLLATE NOCASE ORDER BY priority") fun getAllMeaningsByWord(word: String): List -} +} \ No newline at end of file diff --git a/app/src/main/java/com/xtreak/notificationdictionary/daos/HistoryDao.kt b/app/src/main/java/com/xtreak/notificationdictionary/daos/HistoryDao.kt new file mode 100644 index 0000000..16e0859 --- /dev/null +++ b/app/src/main/java/com/xtreak/notificationdictionary/daos/HistoryDao.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021, Karthikeyan Singaravelan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.xtreak.notificationdictionary + +import androidx.room.* + +@Dao +interface HistoryDao { + @Query("SELECT * from history where word = :word") + fun getHistory(word: String): History? + + @Upsert + fun insertHistory(history: History) + + @Query("UPDATE history set last_accessed_at = :lastAccessedAt where word = :word") + fun updateHistory(lastAccessedAt: Long, word: String) + + @Query("DELETE from history where word = :word") + fun deleteHistory(word: String) + + @Query("SELECT * from history") + fun getAllEntries(): List + + @Query("select h.word, h.is_favourite as isFavourite, h.last_accessed_at as lastAccessedAt, d.definition, d.lexical_category as lexicalCategory from history as h join dictionary as d where h.word = d.word group by h.word order by h.last_accessed_at desc;") + fun getAllEntriesWithMeaning(): List + + @Query("select h.word, h.is_favourite as isFavourite, h.last_accessed_at as lastAccessedAt, d.definition, d.lexical_category as lexicalCategory from history as h join dictionary as d where h.word = d.word and h.is_favourite = 1 group by h.word order by h.last_accessed_at desc;") + fun getAllFavouriteEntriesWithMeaning(): List + + @Query("UPDATE history set is_favourite = 1 where word = :word") + fun addFavourite(word: String) + + @Query("UPDATE history set is_favourite = 0 where word = :word") + fun removeFavourite(word: String) + + @Query("SELECT * from history where is_favourite = 1") + fun getFavouriteEntries(): List + + class WordWithMeaning(var isFavourite: Int? = 0, var word: String? = "", + var definition: String? = "" + ) { + var lastAccessedAt: Int? = 0 + var lexicalCategory: String? = "" + } +} diff --git a/app/src/main/java/com/xtreak/notificationdictionary/database/AppDatabase.kt b/app/src/main/java/com/xtreak/notificationdictionary/database/AppDatabase.kt index e097f84..3fc75d9 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/database/AppDatabase.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/database/AppDatabase.kt @@ -16,12 +16,15 @@ import android.util.Log import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import java.io.File -@Database(entities = [Word::class], version = 1, exportSchema = false) +@Database(entities = [Word::class, History::class], version = 2, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun dictionaryDao(): DictionaryDao + abstract fun historyDao(): HistoryDao companion object { @@ -31,6 +34,14 @@ abstract class AppDatabase : RoomDatabase() { private var INSTANCE: AppDatabase? = null private var DATABASE_NAME: String? = null + private val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + Log.d("ndict:", "Migration started") + database.execSQL("CREATE TABLE IF NOT EXISTS history(id INTEGER primary key, word TEXT, is_favourite INTEGER default 0 NOT NULL, last_accessed_at INTEGER)") + Log.d("ndict:", "Migration completed") + } + } + fun getDatabase(context: Context): AppDatabase { val sharedPref = context.getSharedPreferences( @@ -66,7 +77,7 @@ abstract class AppDatabase : RoomDatabase() { context, AppDatabase::class.java, database_name - ).createFromFile(db_path).build() + ).createFromFile(db_path).addMigrations(MIGRATION_1_2).build() // return instance INSTANCE = instance instance diff --git a/app/src/main/java/com/xtreak/notificationdictionary/entities/Word.kt b/app/src/main/java/com/xtreak/notificationdictionary/entities/Word.kt index f5af435..063769a 100644 --- a/app/src/main/java/com/xtreak/notificationdictionary/entities/Word.kt +++ b/app/src/main/java/com/xtreak/notificationdictionary/entities/Word.kt @@ -29,4 +29,14 @@ data class Word( @ColumnInfo(name = "etymology_no") val etymologyNumber: Int?, @ColumnInfo(name = "definition_no") val definitionNumber: Int?, @ColumnInfo(name = "definition") var definition: String? -) \ No newline at end of file +) + +@Entity( + tableName = "history", +) +data class History( + @PrimaryKey val id: Int?, + @ColumnInfo(name = "word") val word: String?, + @ColumnInfo(name = "is_favourite") var isFavourite: Int = 0, + @ColumnInfo(name = "last_accessed_at") val lastAccessedAt: Long? + ) \ No newline at end of file diff --git a/app/src/main/res/layout/activity_favourite.xml b/app/src/main/res/layout/activity_favourite.xml new file mode 100644 index 0000000..0eb859d --- /dev/null +++ b/app/src/main/res/layout/activity_favourite.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml new file mode 100644 index 0000000..dbf0e69 --- /dev/null +++ b/app/src/main/res/layout/activity_history.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/history_layout.xml b/app/src/main/res/layout/history_layout.xml new file mode 100644 index 0000000..c79fe8c --- /dev/null +++ b/app/src/main/res/layout/history_layout.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index 1dde227..287df0e 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -16,4 +16,13 @@ android:id="@+id/license" android:showAsAction="never" android:title="@string/license" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abb4ce0..e061ba6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,7 @@ Manage Do you want to change the language and download database if not present? Language + Settings + History + Favourites