Skip to content

Commit

Permalink
Copy, share and read aloud meanings (#45)
Browse files Browse the repository at this point in the history
* Add support for Google Keep notes as per Firefox Focus method.

* Implement copy, share and reading meaning using TextToSpeech API.

* Add features to home page.
  • Loading branch information
tirkarthi committed Oct 2, 2023
1 parent a7bf86e commit 077d531
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 42 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ android {
applicationId "com.xtreak.notificationdictionary"
minSdk 24
targetSdk 33
versionCode 20
versionName "0.0.20"
versionCode 21
versionName "0.0.21"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
26 changes: 21 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,36 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xtreak.notificationdictionary">
package="com.xtreak.notificationdictionary" >

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NotificationDictionary">
android:theme="@style/Theme.NotificationDictionary" >

<activity
android:name=".AboutActivity"
android:exported="true"
android:label="@string/about"
android:parentActivityName=".MainActivity" />

<activity
android:name=".ProcessTextActivity"
android:exported="true"
android:label="@string/process_text_label"
android:parentActivityName=".MainActivity">
android:parentActivityName=".MainActivity" >
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

<activity
android:name=".ProcessViewActivity"
android:exported="true"
android:label="@string/process_text_label"
android:parentActivityName=".MainActivity" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>

<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
69 changes: 40 additions & 29 deletions app/src/main/java/com/xtreak/notificationdictionary/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
Expand Down Expand Up @@ -132,22 +131,38 @@ class MainActivity : AppCompatActivity() {
val mListadapter =
RoomAdapter(
listOf(
Word(
1,
"",
"Read meanings aloud as you read",
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."""
),
Word(
1,
"",
"Copy and share",
1,
1,
"""Click on meaning to copy. Long press to share meaning with others. Notifications also have button for these actions."""
),
Word(
1,
"",
"Thanks for the support",
1,
1,
"""The application is open source and free to use. The development is
done in my free time apart from my day job along with download costs for database files
from CDN. If you find the app useful please leave a review in Play store and share the
app with your friends. It will help and encourage me in maintaining the app and adding more features.
done in my free time apart from my day job along with download costs for database files
from CDN. If you find the app useful please leave a review in Play store and share the
app with your friends. It will help and encourage me in maintaining the app and adding more features.
Please grant notification permission since the app requires notification permission in
Android 13+ to show meanings through notification.
Thanks for your support.
"""
)
),
), this
)
mRecyclerView.adapter = mListadapter
Expand Down Expand Up @@ -315,39 +330,35 @@ class MainActivity : AppCompatActivity() {
val inflater = menuInflater
inflater.inflate(R.menu.menu, menu)

val menuItem = menu!!.findItem(R.id.switch_theme)
val view = MenuItemCompat.getActionView(menuItem)
val switchSoundItem = menu!!.findItem(R.id.switch_sound)
val soundView = MenuItemCompat.getActionView(switchSoundItem)
val sharedPref = applicationContext.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE
)
val switch = view.findViewById<View>(R.id.theme_switch_button) as Switch
switch.isChecked = sharedPref.getInt(
"selected_theme",
R.style.Theme_NotificationDictionary
) == R.style.Theme_NotificationDictionary_Dark

val switch_sound = soundView.findViewById<View>(R.id.sound_switch_button) as Switch
var switch_sound_value = sharedPref.getBoolean(
"read_definition",
false
)

switch_sound.isChecked = switch_sound_value


// https://stackoverflow.com/questions/32091709/how-to-get-set-action-event-in-android-actionbar-switch
// https://stackoverflow.com/questions/8811594/implementing-user-choice-of-theme
// https://stackoverflow.com/questions/2482848/how-to-change-current-theme-at-runtime-in-android
// recreate needs to be called as per stackoverflow answers after initial theme is set though it's not documented.
switch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
with(sharedPref.edit()) {
putInt("selected_theme", R.style.Theme_NotificationDictionary_Dark)
apply()
commit()
}
setTheme(R.style.Theme_NotificationDictionary_Dark)
recreate()
} else {
with(sharedPref.edit()) {
putInt("selected_theme", R.style.Theme_NotificationDictionary)
apply()
commit()
}
setTheme(R.style.Theme_NotificationDictionary)
recreate()
switch_sound.setOnClickListener { buttonView ->
val sound_button = findViewById<View>(R.id.sound_switch_button) as Switch
switch_sound_value = !switch_sound_value
with(sharedPref.edit()) {
putBoolean("read_definition", switch_sound_value)
apply()
commit()
}

sound_button.isChecked = switch_sound_value
}
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,42 @@
package com.xtreak.notificationdictionary

import android.app.PendingIntent
import android.content.Intent
import android.content.*
import android.os.Build
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit


class ProcessTextActivity : AppCompatActivity() {
private class TTSOnInitListener(
private val in_word: String,
private val in_definition: String,
private val context: Context
) : TextToSpeech.OnInitListener {
val tts = TextToSpeech(context, this)

override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
tts.language = Locale.getDefault()
Log.d("ndict current locale", Locale.getDefault().language)
tts.speak("$in_word, $in_definition", TextToSpeech.QUEUE_FLUSH, null)
}
}
}

open class ProcessIntentActivity : AppCompatActivity() {

private val CHANNEL_ID = "Dictionary"
private val CHANNEL_NUMBER = 1
private val NOTIFICATION_TIMEOUT = 20000


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -44,7 +65,7 @@ class ProcessTextActivity : AppCompatActivity() {
try {
meaning = dao.getMeaningsByWord(word, 1)
if (meaning != null) {
resolveRedirectMeaning(listOf(meaning) as List<Word>, dao)
resolveRedirectMeaning(listOf(meaning), dao)
}
} catch (e: Exception) {
meaning = Word(
Expand All @@ -71,7 +92,13 @@ class ProcessTextActivity : AppCompatActivity() {
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setSound(null) // sound is set null but still the notification importance level seems to trigger sound
.setAutoCancel(true)
.setTimeoutAfter(20000)
.setTimeoutAfter(NOTIFICATION_TIMEOUT.toLong())

if (definition != "No meaning found") {
addCopyButton(word, definition, context, builder)
addShareButton(word, definition, context, builder)
addReadButton(word, definition, context, builder)
}

val intent = Intent(
context, MainActivity::class.java
Expand All @@ -95,8 +122,147 @@ class ProcessTextActivity : AppCompatActivity() {
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(CHANNEL_NUMBER, builder.build())

val sharedPref = applicationContext.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE
)
val read_definition = sharedPref.getBoolean(
"read_definition",
false
)

if (read_definition) {
TTSOnInitListener(word, definition, context)
}
// Android intent filters should have an activity but we need to raise only a notification, so call finish
// When the app is not open in background or actively running the white screen appears for a second or so.
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) {
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)
sharingIntent.type = "text/plain"
val content =
"${word}\n\n${definition}\n\nSent via Notification Dictionary (https://play.google.com/store/apps/details?id=com.xtreak.notificationdictionary)"
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, word)
sharingIntent.putExtra(Intent.EXTRA_TEXT, content)
startActivity(Intent.createChooser(sharingIntent, "Share via"))

// unregister the receiver else they will keep adding themselves to context resulting in duplicate calls
try {
context.unregisterReceiver(this)
} catch (e: IllegalArgumentException) {
Log.e("Notification Dictionary", "Error in unregistering the receiver")
}
}
}

val intentFilter = IntentFilter("com.xtreak.notificationdictionary.ACTION_SHARE")
context.registerReceiver(notificationShare, intentFilter)

val share = Intent("com.xtreak.notificationdictionary.ACTION_SHARE")
val nShare =
PendingIntent.getBroadcast(
context,
0,
share,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

builder.addAction(
NotificationCompat.Action(
android.R.drawable.ic_menu_share,
"Share",
nShare
)
)
}

private fun addReadButton(
word: String,
definition: String,
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)

// unregister the receiver else they will keep adding themselves to context resulting in duplicate calls
try {
context.unregisterReceiver(this)
} catch (e: IllegalArgumentException) {
Log.e("Notification Dictionary", "Error in unregistering the receiver")
}
}
}

val intentFilter = IntentFilter("com.xtreak.notificationdictionary.ACTION_TTS")
context.registerReceiver(notificationRead, intentFilter)

val read = Intent("com.xtreak.notificationdictionary.ACTION_TTS")
val nRead =
PendingIntent.getBroadcast(
context,
0,
read,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

builder.addAction(NotificationCompat.Action(null, "Read", nRead))
}

}


// Android needs different classes for different intent filters. So add one PROCESS_TEXT and another for VIEW

class ProcessViewActivity : ProcessIntentActivity()

class ProcessTextActivity : ProcessIntentActivity()
Loading

0 comments on commit 077d531

Please sign in to comment.