Skip to content

Commit 4238e36

Browse files
Merge pull request #152 from Ayush0Chaudhary/freemium
🧠 implemented the tap to stop panda, implement bring your own key
2 parents cb03d80 + 3ffe9ff commit 4238e36

File tree

8 files changed

+192
-11
lines changed

8 files changed

+192
-11
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ android {
2525
applicationId = "com.blurr.voice"
2626
minSdk = 24
2727
targetSdk = 35
28-
versionCode = 8
29-
versionName = "1.0.8"
28+
versionCode = 9
29+
versionName = "1.0.9" +
30+
""
3031

3132
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3233
}

app/src/main/assets/prompts/system_prompt.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Strictly follow these rules while using the Android Phone and navigating the app
101101
- You have access to a persistent file system which you can use to track progress, store results, and manage long tasks.
102102
- Your file system is initialized with two files:
103103
1. `todo.md`: Use this to keep a checklist for known subtasks. Update it to mark completed items and track what remains. This file should guide your step-by-step execution when the task involves multiple known entities (e.g., a list of apps or items to visit). The contents of this file will be also visible in your state. ALWAYS use `write_file` to rewrite entire `todo.md` when you want to update your progress. NEVER use `append_file` on `todo.md` as this can explode your context.
104-
2. `results.md`: Use this to accumulate extracted or generated results for the user. Append each new finding clearly and avoid duplication. This file serves as your output log.
104+
2. `results.md`: Use this to accumulate extracted or generated results for the user. Append each new finding clearly and avoid duplication. This file serves as your output log but If user asked explicitly to summarize the screen, you will have to speak the summary using speak action, DONT JUST ADD THE RESULT, you are interacting with human too.
105105
- You can read, write, and append to files.
106106
- Note that `write_file` rewrites the entire file, so make sure to repeat all the existing information if you use this action.
107107
- When you `append_file`, ALWAYS put newlines in the beginning and not at the end.
@@ -127,7 +127,9 @@ The `done` action is your opportunity to terminate and share your findings with
127127
If you are allowed multiple actions:
128128
- You can specify multiple actions in the list to be executed sequentially (one after another). But always specify only one action name per item.
129129
- If the app-screen changes after an action, the sequence is interrupted and you get the new state. You might have to repeat the same action again so that your changes are reflected in the new state.
130-
- ONLY use multiple actions when actions should not change the screen state significantly.
130+
- ONLY use multiple actions when actions should not change the screen state significantly.
131+
- If you think something needs to communicated with the user, please use speak command. For example request like summarize the current screen.
132+
- If user have question about the current screen, don't go to another app.
131133

132134
If you are allowed 1 action, ALWAYS output only 1 most reasonable action per step. If you have something in your read_state, always prioritize saving the data first.
133135
</action_rules>

app/src/main/java/com/blurr/voice/ConversationalAgentService.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class ConversationalAgentService : Service() {
9898
usedMemories.clear() // Clear used memories for new conversation
9999
visualFeedbackManager.showTtsWave()
100100
showInputBoxIfNeeded()
101+
visualFeedbackManager.showSpeakingOverlay() // <-- ADD THIS LINE
101102

102103

103104
}
@@ -115,6 +116,11 @@ class ConversationalAgentService : Service() {
115116
// This is the existing callback for when text is submitted
116117
processUserInput(submittedText)
117118
},
119+
onOutsideTap = {
120+
serviceScope.launch {
121+
instantShutdown()
122+
}
123+
}
118124
)
119125
}
120126

@@ -694,6 +700,9 @@ class ConversationalAgentService : Service() {
694700
}
695701

696702
private suspend fun gracefulShutdown(exitMessage: String? = null) {
703+
visualFeedbackManager.hideTtsWave()
704+
visualFeedbackManager.hideTranscription()
705+
visualFeedbackManager.hideSpeakingOverlay()
697706
visualFeedbackManager.hideInputBox()
698707

699708
if (exitMessage != null) {
@@ -710,13 +719,40 @@ class ConversationalAgentService : Service() {
710719

711720
}
712721

722+
/**
723+
* Immediately stops all TTS, STT, and background tasks, hides all UI, and stops the service.
724+
* This is used for forceful termination, like an outside tap.
725+
*/
726+
private suspend fun instantShutdown() {
727+
// We use the mainHandler to ensure these operations run on the UI thread.
728+
// mainHandler.post {
729+
Log.d("ConvAgent", "Instant shutdown triggered by user.")
730+
speechCoordinator.stopSpeaking()
731+
speechCoordinator.stopListening()
732+
visualFeedbackManager.hideTtsWave()
733+
visualFeedbackManager.hideTranscription()
734+
visualFeedbackManager.hideSpeakingOverlay()
735+
visualFeedbackManager.hideInputBox()
736+
737+
removeClarificationQuestions()
738+
// Make a thread-safe copy of the conversation history.
739+
if (conversationHistory.size > 1) {
740+
Log.d("ConvAgent", "Extracting memories before shutdown.")
741+
MemoryExtractor.extractAndStoreMemories(conversationHistory, memoryManager, usedMemories)
742+
}
743+
serviceScope.cancel("User tapped outside, forcing instant shutdown.")
744+
745+
stopSelf()
746+
// }
747+
}
713748
override fun onDestroy() {
714749
super.onDestroy()
715750
Log.d("ConvAgent", "Service onDestroy")
716751
removeClarificationQuestions()
717752
serviceScope.cancel()
718753
ttsManager.setCaptionsEnabled(false)
719754
isRunning = false
755+
visualFeedbackManager.hideSpeakingOverlay() // <-- ADD THIS LINE
720756
// USE the new manager to hide the wave and transcription view
721757
visualFeedbackManager.hideTtsWave()
722758
visualFeedbackManager.hideTranscription()

app/src/main/java/com/blurr/voice/SettingsActivity.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.core.content.edit
1515
import androidx.lifecycle.lifecycleScope
1616
import com.blurr.voice.agent.v1.VisionMode
1717
import com.blurr.voice.api.GoogleTts
18+
import com.blurr.voice.api.PicovoiceKeyManager
1819
import com.blurr.voice.api.TTSVoice
1920
import com.blurr.voice.utilities.SpeechCoordinator
2021
import com.blurr.voice.utilities.VoicePreferenceManager
@@ -35,6 +36,8 @@ class SettingsActivity : AppCompatActivity() {
3536
private lateinit var visionModeDescription: TextView
3637
private lateinit var editUserName: android.widget.EditText
3738
private lateinit var editUserEmail: android.widget.EditText
39+
private lateinit var editWakeWordKey: android.widget.EditText
40+
private lateinit var buttonSaveWakeWordKey: Button
3841

3942
private lateinit var sc: SpeechCoordinator
4043
private lateinit var sharedPreferences: SharedPreferences
@@ -78,6 +81,8 @@ class SettingsActivity : AppCompatActivity() {
7881
backButton = findViewById(R.id.id_backButtonSettings)
7982
permissionsInfoButton = findViewById(R.id.permissionsInfoButton)
8083
visionModeGroup = findViewById(R.id.visionModeGroup)
84+
editWakeWordKey = findViewById(R.id.editWakeWordKey)
85+
buttonSaveWakeWordKey = findViewById(R.id.buttonSaveWakeWordKey)
8186
visionModeDescription = findViewById(R.id.visionModeDescription)
8287
editUserName = findViewById(R.id.editUserName)
8388
editUserEmail = findViewById(R.id.editUserEmail)
@@ -107,6 +112,12 @@ class SettingsActivity : AppCompatActivity() {
107112
val intent = Intent(this, PermissionsActivity::class.java)
108113
startActivity(intent)
109114
}
115+
buttonSaveWakeWordKey.setOnClickListener {
116+
val userKey = editWakeWordKey.text.toString().trim()
117+
val keyManager = PicovoiceKeyManager(this)
118+
keyManager.saveUserProvidedKey(userKey) // You will create this method next
119+
Toast.makeText(this, "Wake word key saved.", Toast.LENGTH_SHORT).show()
120+
}
110121
}
111122

112123
private fun setupAutoSavingListeners() {
@@ -192,6 +203,10 @@ class SettingsActivity : AppCompatActivity() {
192203
}
193204

194205
private fun loadAllSettings() {
206+
207+
// Inside loadAllSettings()
208+
val keyManager = PicovoiceKeyManager(this)
209+
editWakeWordKey.setText(keyManager.getUserProvidedKey() ?: "") // You will create this method next
195210
val savedVoiceName = sharedPreferences.getString(KEY_SELECTED_VOICE, DEFAULT_VOICE.name)
196211
val savedVoice = availableVoices.find { it.name == savedVoiceName } ?: DEFAULT_VOICE
197212
ttsVoicePicker.value = availableVoices.indexOf(savedVoice)

app/src/main/java/com/blurr/voice/api/PicovoiceKeyManager.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class PicovoiceKeyManager(private val context: Context) {
2626
private const val TAG = "PicovoiceKeyManager"
2727
private const val PREFS_NAME = "PicovoicePrefs"
2828
private const val KEY_ACCESS_KEY = "access_key"
29-
29+
// Inside companion object
30+
private const val KEY_USER_PROVIDED_KEY = "user_provided_access_key"
3031
private const val API_URL = BuildConfig.GCLOUD_GATEWAY_URL
3132
private const val API_KEY_HEADER = "x-api-key"
3233
private const val API_KEY_VALUE = BuildConfig.GCLOUD_GATEWAY_PICOVOICE_KEY
@@ -44,6 +45,11 @@ class PicovoiceKeyManager(private val context: Context) {
4445
*/
4546
suspend fun getAccessKey(): String? = withContext(Dispatchers.IO) {
4647
try {
48+
val userKey = getUserProvidedKey()
49+
if (!userKey.isNullOrBlank()) {
50+
Log.d(TAG, "Using user-provided Picovoice access key")
51+
return@withContext userKey
52+
}
4753
// Check if we have a cached key
4854
val cachedKey = getCachedAccessKey()
4955
if (cachedKey != null) {
@@ -151,4 +157,20 @@ class PicovoiceKeyManager(private val context: Context) {
151157
}
152158
Log.d(TAG, "Cleared cached Picovoice access key")
153159
}
160+
161+
/**
162+
* Saves a key provided by the user to SharedPreferences.
163+
*/
164+
fun saveUserProvidedKey(accessKey: String) {
165+
sharedPreferences.edit {
166+
putString(KEY_USER_PROVIDED_KEY, accessKey)
167+
}
168+
}
169+
170+
/**
171+
* Gets the key provided by the user from SharedPreferences.
172+
*/
173+
fun getUserProvidedKey(): String? {
174+
return sharedPreferences.getString(KEY_USER_PROVIDED_KEY, null)
175+
}
154176
}

app/src/main/java/com/blurr/voice/utilities/VisualFeedbackManager.kt

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class VisualFeedbackManager private constructor(private val context: Context) {
3535
private var transcriptionView: TextView? = null
3636
private var inputBoxView: View? = null
3737

38+
private var speakingOverlay: View? = null
3839

3940
companion object {
4041
private const val TAG = "VisualFeedbackManager"
@@ -73,6 +74,8 @@ class VisualFeedbackManager private constructor(private val context: Context) {
7374
ttsVisualizer?.stop()
7475
ttsVisualizer = null
7576
TTSManager.getInstance(context).utteranceListener = null
77+
hideSpeakingOverlay()
78+
7679
Log.d(TAG, "Audio wave effect has been torn down.")
7780
}
7881
}
@@ -124,6 +127,34 @@ class VisualFeedbackManager private constructor(private val context: Context) {
124127
Log.d(TAG, "Audio wave effect has been set up.")
125128
}
126129

130+
fun showSpeakingOverlay() {
131+
mainHandler.post {
132+
if (speakingOverlay != null) return@post
133+
134+
speakingOverlay = View(context).apply {
135+
// CHANGED: Increased opacity from 80 (50%) to E6 (90%) for a more solid feel.
136+
// You can adjust this hex value (E6) to your liking.
137+
setBackgroundColor(0x80FFFFFF.toInt())
138+
}
139+
140+
val params = WindowManager.LayoutParams(
141+
WindowManager.LayoutParams.MATCH_PARENT,
142+
WindowManager.LayoutParams.MATCH_PARENT,
143+
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
144+
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
145+
PixelFormat.TRANSLUCENT
146+
)
147+
148+
try {
149+
windowManager.addView(speakingOverlay, params)
150+
Log.d(TAG, "Speaking overlay added.")
151+
} catch (e: Exception) {
152+
Log.e(TAG, "Error adding speaking overlay", e)
153+
}
154+
}
155+
}
156+
157+
127158
fun showTranscription(initialText: String = "Listening...") {
128159
if (transcriptionView != null) {
129160
updateTranscription(initialText) // Update text if already shown
@@ -193,7 +224,8 @@ class VisualFeedbackManager private constructor(private val context: Context) {
193224
@SuppressLint("ClickableViewAccessibility")
194225
fun showInputBox(
195226
onActivated: () -> Unit,
196-
onSubmit: (String) -> Unit
227+
onSubmit: (String) -> Unit,
228+
onOutsideTap: () -> Unit
197229
) {
198230
mainHandler.post {
199231
if (inputBoxView?.isAttachedToWindow == true) {
@@ -217,9 +249,9 @@ class VisualFeedbackManager private constructor(private val context: Context) {
217249
val params = WindowManager.LayoutParams(
218250
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT,
219251
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
220-
// You need it to be focusable to receive key events.
221-
// The absence of FLAG_NOT_FOCUSABLE makes it focusable by default.
222-
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
252+
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
253+
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
254+
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
223255
PixelFormat.TRANSLUCENT
224256
).apply {
225257
gravity = Gravity.BOTTOM
@@ -264,7 +296,8 @@ class VisualFeedbackManager private constructor(private val context: Context) {
264296

265297
rootLayout?.setOnTouchListener { _, event ->
266298
if (event.action == MotionEvent.ACTION_OUTSIDE) {
267-
hideInputBox()
299+
Log.d(TAG, "Outside touch detected.")
300+
onOutsideTap() // Use the new callback
268301
return@setOnTouchListener true
269302
}
270303
false
@@ -295,4 +328,19 @@ class VisualFeedbackManager private constructor(private val context: Context) {
295328
inputBoxView = null
296329
}
297330
}
331+
fun hideSpeakingOverlay() {
332+
mainHandler.post {
333+
speakingOverlay?.let {
334+
if (it.isAttachedToWindow) {
335+
try {
336+
windowManager.removeView(it)
337+
Log.d(TAG, "Speaking overlay removed.")
338+
} catch (e: Exception) {
339+
Log.e(TAG, "Error removing speaking overlay", e)
340+
}
341+
}
342+
}
343+
speakingOverlay = null
344+
}
345+
}
298346
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:shape="rectangle">
4+
5+
<solid android:color="#333333" />
6+
7+
<stroke
8+
android:width="1dp"
9+
android:color="#888888" /> <corners android:radius="12dp" />
10+
11+
</shape>

app/src/main/res/layout/activity_settings.xml

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,53 @@
8585
</FrameLayout>
8686

8787

88+
</LinearLayout>
89+
<LinearLayout
90+
android:layout_width="match_parent"
91+
android:layout_height="wrap_content"
92+
android:layout_marginTop="16dp"
93+
android:background="@drawable/rounded_background"
94+
android:orientation="vertical"
95+
android:padding="16dp">
96+
97+
<TextView
98+
android:layout_width="wrap_content"
99+
android:layout_height="wrap_content"
100+
android:text="Wake Word Service Key"
101+
android:textColor="@color/white"
102+
android:textSize="18sp"
103+
android:textStyle="bold" />
104+
105+
<TextView
106+
android:layout_width="wrap_content"
107+
android:layout_height="wrap_content"
108+
android:layout_marginTop="4dp"
109+
android:text="If you have your own Picovoice AccessKey, you can paste it here."
110+
android:textColor="#888888"
111+
android:textSize="12sp" />
112+
113+
<EditText
114+
android:id="@+id/editWakeWordKey"
115+
android:layout_width="match_parent"
116+
android:layout_height="wrap_content"
117+
android:layout_marginTop="12dp"
118+
android:background="@drawable/edit_text_border"
119+
android:hint="Paste your key here"
120+
android:inputType="text"
121+
android:padding="12dp"
122+
android:textColor="#CECECE"
123+
android:textColorHint="#888888" />
124+
125+
<Button
126+
android:id="@+id/buttonSaveWakeWordKey"
127+
android:layout_width="wrap_content"
128+
android:layout_height="wrap_content"
129+
android:layout_gravity="end"
130+
android:layout_marginTop="12dp"
131+
android:text="Save Key"
132+
android:background="@drawable/btn_with_border"
133+
android:textColor="@android:color/white" />
134+
88135
</LinearLayout>
89136

90137
<LinearLayout
@@ -203,7 +250,6 @@
203250
</LinearLayout>
204251

205252

206-
207253
<TextView
208254
android:id="@+id/permissionsInfoButton"
209255
android:layout_width="wrap_content"

0 commit comments

Comments
 (0)