Skip to content

Commit e5b437c

Browse files
Feature/purchase thank you message (#24)
* feat: Display thank you message on purchase I modified MainScreen (MenuScreen.kt) to display a "Thank you" message when the app has been purchased. Changes: - Added `isPurchased` parameter to `MenuScreen`. - `MainActivity` now passes the purchase status (`currentTrialState == TrialManager.TrialState.PURCHASED`) to `MenuScreen`. - In `MenuScreen`, if `isPurchased` is true, the donation button and related text are replaced with "Thank you for supporting the development! 🎉💛". - Added a new preview for the purchased state in `MenuScreen.kt`. * System message dynamic height (#23) * feat: Adjust system message height and back button behavior Implements dynamic height changes for the system message text field in the PhotoReasoningScreen based on focus and keyboard visibility: - Focused + Keyboard open: 600dp - Focused + Keyboard closed: 1000dp - Not focused: 120dp Additionally, modifies back button behavior: - If the system message field is focused with the keyboard closed (1000dp height), the first back press deselects the field, changing its height to 120dp. - Subsequent back presses perform the default navigation. Keyboard visibility is detected in MainActivity and propagated to the PhotoReasoningScreen. * fix: Add missing import for onFocusChanged Adds the import `androidx.compose.ui.focus.onFocusChanged` to `PhotoReasoningScreen.kt` to resolve a build compilation error. * fix: Correct system message TextField behavior Addresses your feedback on the system message TextField in PhotoReasoningScreen: - Sets focused height with keyboard to 450dp (was 600dp). - Dynamically adjusts minLines and maxLines of the OutlinedTextField to ensure the text input area expands with the component's height. - Modifies the BackHandler to explicitly clear focus from the TextField (in addition to collapsing it) when it's focused without the keyboard and back is pressed. This ensures it can be re-expanded correctly on subsequent focus. These changes improve the usability and appearance of the system message input field. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent c687483 commit e5b437c

File tree

3 files changed

+94
-21
lines changed

3 files changed

+94
-21
lines changed

app/src/main/kotlin/com/google/ai/sample/MainActivity.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import android.content.SharedPreferences
1111
import android.content.pm.PackageManager
1212
import android.net.Uri
1313
import android.os.Build
14+
import android.graphics.Rect
1415
import android.os.Bundle
1516
import android.provider.Settings
1617
import android.util.Log
18+
import android.view.View
19+
import android.view.ViewTreeObserver
1720
import android.widget.Toast
1821
import androidx.activity.ComponentActivity
1922
import androidx.activity.compose.setContent
@@ -70,6 +73,11 @@ import kotlinx.coroutines.launch
7073

7174
class MainActivity : ComponentActivity() {
7275

76+
// Keyboard Visibility
77+
private val _isKeyboardOpen = MutableStateFlow(false)
78+
val isKeyboardOpen: StateFlow<Boolean> = _isKeyboardOpen.asStateFlow()
79+
private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
80+
7381
private var photoReasoningViewModel: PhotoReasoningViewModel? = null
7482
private lateinit var apiKeyManager: ApiKeyManager
7583
private var showApiKeyDialog by mutableStateOf(false)
@@ -286,6 +294,26 @@ class MainActivity : ComponentActivity() {
286294
// Initial check for accessibility service status
287295
refreshAccessibilityServiceStatus()
288296

297+
// Keyboard visibility listener
298+
val rootView = findViewById<View>(android.R.id.content)
299+
onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
300+
val rect = Rect()
301+
rootView.getWindowVisibleDisplayFrame(rect)
302+
val screenHeight = rootView.rootView.height
303+
val keypadHeight = screenHeight - rect.bottom
304+
if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is a common threshold
305+
if (!_isKeyboardOpen.value) {
306+
_isKeyboardOpen.value = true
307+
Log.d(TAG, "Keyboard visible")
308+
}
309+
} else {
310+
if (_isKeyboardOpen.value) {
311+
_isKeyboardOpen.value = false
312+
Log.d(TAG, "Keyboard hidden")
313+
}
314+
}
315+
}
316+
rootView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
289317

290318
Log.d(TAG, "onCreate: Calling setContent.")
291319
setContent {
@@ -400,7 +428,8 @@ class MainActivity : ComponentActivity() {
400428
Log.d(TAG, "MenuScreen onDonationButtonClicked: Initiating subscription purchase.")
401429
initiateDonationPurchase()
402430
},
403-
isTrialExpired = currentTrialState == TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED
431+
isPurchased = (currentTrialState == TrialManager.TrialState.PURCHASED),
432+
isTrialExpired = currentTrialState == TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED
404433
)
405434
}
406435
composable("photo_reasoning") {
@@ -751,6 +780,11 @@ class MainActivity : ComponentActivity() {
751780
billingClient.endConnection()
752781
Log.d(TAG, "onDestroy: BillingClient connection ended.")
753782
}
783+
// Remove keyboard listener
784+
onGlobalLayoutListener?.let {
785+
findViewById<View>(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(it)
786+
Log.d(TAG, "onDestroy: Keyboard layout listener removed.")
787+
}
754788
if (this == instance) {
755789
instance = null
756790
Log.d(TAG, "onDestroy: MainActivity instance cleared.")

app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.platform.LocalContext
2626
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.text.style.TextAlign
2728
import androidx.compose.ui.tooling.preview.Preview
2829
import androidx.compose.ui.unit.dp
2930
import androidx.compose.foundation.text.ClickableText
@@ -46,7 +47,8 @@ fun MenuScreen(
4647
onItemClicked: (String) -> Unit = { },
4748
onApiKeyButtonClicked: () -> Unit = { },
4849
onDonationButtonClicked: () -> Unit = { },
49-
isTrialExpired: Boolean = false // New parameter to indicate trial status
50+
isTrialExpired: Boolean = false, // New parameter to indicate trial status
51+
isPurchased: Boolean = false
5052
) {
5153
val context = LocalContext.current
5254
val menuItems = listOf(
@@ -209,16 +211,25 @@ fun MenuScreen(
209211
.fillMaxWidth(),
210212
verticalAlignment = Alignment.CenterVertically
211213
) {
212-
Text(
213-
text = "Support more Features",
214-
style = MaterialTheme.typography.titleMedium,
215-
modifier = Modifier.weight(1f)
216-
)
217-
Button(
218-
onClick = onDonationButtonClicked, // This button should always be active
219-
modifier = Modifier.padding(start = 8.dp)
220-
) {
221-
Text(text = "Pro (2,90 €/Month)")
214+
if (isPurchased) {
215+
Text(
216+
text = "Thank you for supporting the development! 🎉💛",
217+
style = MaterialTheme.typography.titleMedium,
218+
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
219+
textAlign = TextAlign.Center
220+
)
221+
} else {
222+
Text(
223+
text = "Support more Features",
224+
style = MaterialTheme.typography.titleMedium,
225+
modifier = Modifier.weight(1f)
226+
)
227+
Button(
228+
onClick = onDonationButtonClicked,
229+
modifier = Modifier.padding(start = 8.dp)
230+
) {
231+
Text(text = "Pro (2,90 €/Month)")
232+
}
222233
}
223234
}
224235
}
@@ -268,13 +279,19 @@ fun MenuScreen(
268279
@Composable
269280
fun MenuScreenPreview() {
270281
// Preview with trial not expired
271-
MenuScreen(isTrialExpired = false)
282+
MenuScreen(isTrialExpired = false, isPurchased = false)
283+
}
284+
285+
@Preview(showSystemUi = true)
286+
@Composable
287+
fun MenuScreenPurchasedPreview() {
288+
MenuScreen(isTrialExpired = false, isPurchased = true)
272289
}
273290

274291
@Preview(showSystemUi = true)
275292
@Composable
276293
fun MenuScreenTrialExpiredPreview() {
277294
// Preview with trial expired
278-
MenuScreen(isTrialExpired = true)
295+
MenuScreen(isTrialExpired = true, isPurchased = false)
279296
}
280297

app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.graphics.drawable.BitmapDrawable
66
import android.net.Uri
77
import android.provider.Settings
88
import android.widget.Toast // Added for Toast message
9+
import androidx.activity.compose.BackHandler
910
import androidx.activity.compose.rememberLauncherForActivityResult
1011
import androidx.activity.result.PickVisualMediaRequest
1112
import androidx.activity.result.contract.ActivityResultContracts
@@ -49,9 +50,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
4950
import androidx.compose.runtime.setValue
5051
import androidx.compose.ui.Alignment
5152
import androidx.compose.ui.Modifier
53+
import androidx.compose.ui.focus.onFocusChanged
5254
import androidx.compose.ui.draw.drawBehind
5355
import androidx.compose.ui.graphics.Color
5456
import androidx.compose.ui.platform.LocalContext
57+
import androidx.compose.ui.platform.LocalFocusManager
5558
import androidx.compose.ui.res.stringResource
5659
import androidx.compose.ui.tooling.preview.Preview
5760
import androidx.compose.ui.unit.dp
@@ -88,6 +91,7 @@ internal fun PhotoReasoningRoute(
8891

8992
// Observe the accessibility service status from MainActivity
9093
val isAccessibilityServiceEffectivelyEnabled by mainActivity?.isAccessibilityServiceEnabledFlow?.collectAsState() ?: mutableStateOf(false)
94+
val isKeyboardOpen by mainActivity?.isKeyboardOpen?.collectAsState() ?: mutableStateOf(false)
9195

9296
// Launcher for opening accessibility settings
9397
val accessibilitySettingsLauncher = rememberLauncherForActivityResult(
@@ -168,7 +172,8 @@ internal fun PhotoReasoningRoute(
168172
val vm = it.getPhotoReasoningViewModel()
169173
vm?.clearChatHistory(context)
170174
}
171-
}
175+
},
176+
isKeyboardOpen = isKeyboardOpen
172177
)
173178
}
174179

@@ -183,12 +188,20 @@ fun PhotoReasoningScreen(
183188
onReasonClicked: (String, List<Uri>) -> Unit = { _, _ -> },
184189
isAccessibilityServiceEnabled: Boolean = false,
185190
onEnableAccessibilityService: () -> Unit = {},
186-
onClearChatHistory: () -> Unit = {}
191+
onClearChatHistory: () -> Unit = {},
192+
isKeyboardOpen: Boolean
187193
) {
188194
var userQuestion by rememberSaveable { mutableStateOf("") }
189195
val imageUris = rememberSaveable(saver = UriSaver()) { mutableStateListOf() }
196+
var isSystemMessageFocused by rememberSaveable { mutableStateOf(false) }
190197
val listState = rememberLazyListState()
191198
val context = LocalContext.current // Get context for Toast
199+
val focusManager = LocalFocusManager.current
200+
201+
BackHandler(enabled = isSystemMessageFocused && !isKeyboardOpen) {
202+
focusManager.clearFocus() // Clear focus first
203+
isSystemMessageFocused = false // Then update the state that controls height
204+
}
192205

193206
val pickMedia = rememberLauncherForActivityResult(
194207
ActivityResultContracts.PickVisualMedia()
@@ -223,15 +236,23 @@ fun PhotoReasoningScreen(
223236
color = MaterialTheme.colorScheme.onPrimaryContainer
224237
)
225238
Spacer(modifier = Modifier.height(8.dp))
239+
val systemMessageHeight = when {
240+
isSystemMessageFocused && isKeyboardOpen -> 450.dp // Changed from 600.dp
241+
isSystemMessageFocused && !isKeyboardOpen -> 1000.dp
242+
else -> 120.dp
243+
}
244+
val currentMinLines = if (systemMessageHeight == 120.dp) 3 else 1
245+
val currentMaxLines = if (systemMessageHeight == 120.dp) 5 else Int.MAX_VALUE
226246
OutlinedTextField(
227247
value = systemMessage,
228248
onValueChange = onSystemMessageChanged,
229249
placeholder = { Text("Enter a system message here that will be sent with every request") },
230250
modifier = Modifier
231251
.fillMaxWidth()
232-
.height(120.dp),
233-
maxLines = 5,
234-
minLines = 3
252+
.height(systemMessageHeight)
253+
.onFocusChanged { focusState -> isSystemMessageFocused = focusState.isFocused },
254+
minLines = currentMinLines,
255+
maxLines = currentMaxLines
235256
)
236257
}
237258
}
@@ -633,13 +654,14 @@ fun PhotoReasoningScreenPreviewWithContent() {
633654
text = "I am here to help you. What do you want to know?",
634655
participant = PhotoParticipant.MODEL
635656
)
636-
)
657+
),
658+
isKeyboardOpen = false
637659
)
638660
}
639661

640662
@Composable
641663
@Preview(showSystemUi = true)
642664
fun PhotoReasoningScreenPreviewEmpty() {
643-
PhotoReasoningScreen()
665+
PhotoReasoningScreen(isKeyboardOpen = false)
644666
}
645667

0 commit comments

Comments
 (0)