diff --git a/Android/src/app/src/main/AndroidManifest.xml b/Android/src/app/src/main/AndroidManifest.xml index 15e0de78d..cec1b2edf 100644 --- a/Android/src/app/src/main/AndroidManifest.xml +++ b/Android/src/app/src/main/AndroidManifest.xml @@ -30,6 +30,10 @@ + + + + @@ -140,6 +144,18 @@ + + + + + + + + diff --git a/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md b/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md new file mode 100644 index 000000000..d29bd3b01 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md @@ -0,0 +1,26 @@ +--- +name: schedule-notification +description: Schedule a notification for a specific date or repeating daily. +--- + +# Schedule Notification + +## Instructions + +To schedule a notification, you must follow these exact steps: +1. First, if the notification doesn't need to repeat daily, call the `run_intent` tool with `intent` as `get_current_date_and_time` and `parameters` as `{}` to get the user's local date and time. Then explicitly calculate the scheduling date and time in your response. Write out: +- Today's exact date. +- The target day or relative time requested by the user (e.g., "tomorrow", "this Friday"). +- The exact number of days you need to add to today's date. +- The final calculated dates, ensuring you correctly roll over to the next month or year if the added days exceed the days in the current month. +2. Call the `run_intent` tool with the following exact parameters: +- intent: schedule_notification +- parameters: A JSON string with the following fields: + - title: the title of the notification. String. + - message: the message content of the notification. String. + - hour: the hour of the day (0-23) for the notification. Integer. + - minute: the minute of the hour (0-59) for the notification. Integer. + - year: (optional) the year for the notification. Integer. + - month: (optional) the month (1-12) for the notification. Integer. + - day: (optional) the day of the month (1-31) for the notification. Integer. + - repeat_daily: (optional) true if the notification should repeat daily at this time. Boolean. - deeplink: (optional) the deeplink URI to open when the notification is tapped. String. diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt index 960b76393..1ca3752ef 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt @@ -18,6 +18,7 @@ package com.google.ai.edge.gallery import android.app.Application import com.google.ai.edge.gallery.data.DataStoreRepository +import com.google.ai.edge.gallery.notifications.NotificationScheduleManager import com.google.ai.edge.gallery.ui.theme.ThemeSettings import com.google.firebase.FirebaseApp import dagger.hilt.android.HiltAndroidApp @@ -27,9 +28,13 @@ import javax.inject.Inject class GalleryApplication : Application() { @Inject lateinit var dataStoreRepository: DataStoreRepository + @Inject lateinit var notificationScheduleManager: NotificationScheduleManager override fun onCreate() { super.onCreate() + // Initialize the notification schedule manager to load the scheduled notifications from the + // disk. + notificationScheduleManager.initialize() // Load saved theme. ThemeSettings.themeOverride.value = dataStoreRepository.readTheme() diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/IntentHandler.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/IntentHandler.kt index ae530ea0e..a2678bc19 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/IntentHandler.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/IntentHandler.kt @@ -19,8 +19,11 @@ import android.content.Context import android.content.Intent import android.util.Log import androidx.core.net.toUri +import com.google.ai.edge.gallery.notifications.NotificationScheduleManagerEntryPoint +import com.google.ai.edge.gallery.proto.ScheduledNotification import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi +import dagger.hilt.android.EntryPointAccessors import java.lang.Exception import java.text.SimpleDateFormat import java.util.Date @@ -48,13 +51,27 @@ enum class IntentAction(val action: String) { SEND_EMAIL("send_email"), SEND_SMS("send_sms"), CREATE_CALENDAR_EVENT("create_calendar_event"), - GET_CURRENT_DATE_AND_TIME("get_current_date_and_time"); + GET_CURRENT_DATE_AND_TIME("get_current_date_and_time"), + SCHEDULE_NOTIFICATION("schedule_notification"); companion object { fun from(action: String): IntentAction? = entries.find { it.action == action } } } +@JsonClass(generateAdapter = true) +data class ScheduleNotificationParams( + val title: String, + val message: String, + val hour: Int, + val minute: Int, + val year: Int? = null, + val month: Int? = null, + val day: Int? = null, + val repeat_daily: Boolean? = null, + val deeplink: String? = null, +) + object IntentHandler { private const val TAG = "IntentHandler" @@ -142,7 +159,64 @@ object IntentHandler { ) currentDateAndTime } + IntentAction.SCHEDULE_NOTIFICATION -> { + scheduleNotification(context, parameters) + } null -> "failed" } } + + fun scheduleNotification(context: Context, parameters: String): String { + try { + val moshi = Moshi.Builder().build() + val jsonAdapter = moshi.adapter(ScheduleNotificationParams::class.java) + val params = jsonAdapter.fromJson(parameters) + if (params != null) { + val notificationProtoBuilder = + ScheduledNotification.newBuilder() + .setId(java.util.UUID.randomUUID().toString()) + .setTitle(params.title) + .setMessage(params.message) + .setHour(params.hour) + .setMinute(params.minute) + .setChannelId("agent_skill_tasks_channel") + .setChannelName("Agent Skill Task") + if (params.year != null) { + notificationProtoBuilder.setYear(params.year) + } + if (params.month != null) { + notificationProtoBuilder.setMonth(params.month) + } + if (params.day != null) { + notificationProtoBuilder.setDay(params.day) + } + if (params.deeplink != null) { + notificationProtoBuilder.setDeeplink(params.deeplink) + } + if (params.repeat_daily != null) { + notificationProtoBuilder.setRepeatDaily(params.repeat_daily) + } + + val entryPoint = + EntryPointAccessors.fromApplication( + context.applicationContext, + NotificationScheduleManagerEntryPoint::class.java, + ) + val success = + entryPoint + .notificationScheduleManager() + .scheduleNotification(notificationProtoBuilder.build()) + if (!success) { + return "failed" + } + return "succeeded" + } else { + Log.e(TAG, "Failed to parse schedule_notification parameters: $parameters") + return "failed" + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse schedule_notification parameters: $parameters", e) + return "failed" + } + } } diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/SkillManagerViewModel.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/SkillManagerViewModel.kt index d4015d72c..6faf3d29e 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/SkillManagerViewModel.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/SkillManagerViewModel.kt @@ -192,8 +192,8 @@ constructor( try { val skillAssetDirs = context.assets.list("skills") ?: emptyArray() for (dirName in skillAssetDirs) { - // Temporarily disable this skill in built-in skills. - if (dirName == "create-calendar-event") { + // TODO: Temporarily disable some built-in skills. Enable them when ready. + if (dirName == "create-calendar-event" || dirName == "schedule-notification") { continue } val skillMdPath = "skills/$dirName/SKILL.md" diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/BootReceiver.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/BootReceiver.kt new file mode 100644 index 000000000..c25464846 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/BootReceiver.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ai.edge.gallery.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import dagger.hilt.android.EntryPointAccessors + +/** + * Reschedules all notifications after the device boots up. + * + * This receiver is triggered by the ACTION_BOOT_COMPLETED broadcast. + */ +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + Log.d(TAG, "Boot completed received, rescheduling notifications") + try { + val entryPoint = + EntryPointAccessors.fromApplication( + context.applicationContext, + NotificationScheduleManagerEntryPoint::class.java, + ) + entryPoint.notificationScheduleManager().rescheduleAllNotifications() + } catch (e: Exception) { + Log.e(TAG, "Failed to reschedule notifications on boot", e) + } + } + } + + companion object { + private const val TAG = "BootReceiver" + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationPendingIntentHelper.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationPendingIntentHelper.kt new file mode 100644 index 000000000..2bba5e8e8 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationPendingIntentHelper.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ai.edge.gallery.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent + +// Helper class for building a PendingIntent for a notification. +object NotificationPendingIntentHelper { + const val EXTRA_ID = "id" + const val EXTRA_TITLE = "title" + const val EXTRA_MESSAGE = "message" + const val EXTRA_DEEPLINK = "deeplink" + const val EXTRA_REPEAT_DAILY = "repeat_daily" + const val EXTRA_HOUR = "hour" + const val EXTRA_MINUTE = "minute" + const val EXTRA_CHANNEL_ID = "channel_id" + const val EXTRA_CHANNEL_NAME = "channel_name" + + fun buildNotificationPendingIntent( + context: Context, + id: String, + title: String, + message: String, + deeplink: String, + repeatDaily: Boolean, + hour: Int, + minute: Int, + channelId: String, + channelName: String, + ): PendingIntent { + val receiverClass = + Class.forName("com.google.ai.edge.gallery.notifications.NotificationReceiver") + val intent = + Intent(context, receiverClass).apply { + putExtra(EXTRA_ID, id) + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_MESSAGE, message) + putExtra(EXTRA_DEEPLINK, deeplink) + putExtra(EXTRA_REPEAT_DAILY, repeatDaily) + putExtra(EXTRA_HOUR, hour) + putExtra(EXTRA_MINUTE, minute) + putExtra(EXTRA_CHANNEL_ID, channelId) + putExtra(EXTRA_CHANNEL_NAME, channelName) + } + return PendingIntent.getBroadcast( + context, + id.hashCode(), + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationReceiver.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationReceiver.kt new file mode 100644 index 000000000..1c38ee2ee --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationReceiver.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ai.edge.gallery.notifications + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.net.toUri +import dagger.hilt.android.EntryPointAccessors + +class NotificationReceiver : BroadcastReceiver() { + private val DEFAULT_CHANNEL_ID = "ai_edge_gallery_notification_channel" + private val DEFAULT_CHANNEL_NAME = "AI Edge Gallery Notifications" + + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "onReceive called with intent: $intent") + val id = intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_ID) ?: "" + val title = + intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_TITLE) ?: "Scheduled task" + val message = + intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_MESSAGE) + ?: "Time to complete your task!" + val deeplink = intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_DEEPLINK) ?: "" + val channelId = + intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_CHANNEL_ID) ?: DEFAULT_CHANNEL_ID + val channelName = + intent.getStringExtra(NotificationPendingIntentHelper.EXTRA_CHANNEL_NAME) + ?: DEFAULT_CHANNEL_NAME + + try { + // Create the intent for when tapping the notification + val contentIntent = + Intent(Intent.ACTION_VIEW).apply { + if (deeplink.isNotEmpty()) { + data = deeplink.toUri() + } + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = + PendingIntent.getActivity( + context, + 0, + contentIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + } + + val notificationBuilder = + NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(message) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + notificationManager.notify(System.currentTimeMillis().toInt(), notificationBuilder.build()) + // If the notification is not repeating, remove it from the schedule after it is sent. + if ( + !intent.getBooleanExtra(NotificationPendingIntentHelper.EXTRA_REPEAT_DAILY, false) && + id.isNotEmpty() + ) { + val entryPoint = + EntryPointAccessors.fromApplication( + context.applicationContext, + NotificationScheduleManagerEntryPoint::class.java, + ) + entryPoint.notificationScheduleManager().removeNotification(id) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to send notification", e) + } + } + + companion object { + private const val TAG = "NotificationReceiver" + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationScheduleManager.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationScheduleManager.kt new file mode 100644 index 000000000..76d60eff7 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/notifications/NotificationScheduleManager.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ai.edge.gallery.notifications + +import android.app.AlarmManager +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.ScheduledNotification +import com.google.ai.edge.gallery.proto.ScheduledNotifications +import com.google.protobuf.InvalidProtocolBufferException +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.util.Calendar +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +// Manages the scheduled notifications in the agent chat skill, including loading and saving +// notifications to the disk, and canceling/updating scheduled notifications when they are removed. +// This is a global singleton object, and is thread-safe. +@Singleton +class NotificationScheduleManager +@Inject +constructor(@ApplicationContext private val context: Context) { + private val dataStore: DataStore = + DataStoreFactory.create( + serializer = ScheduledNotificationsSerializer, + produceFile = { File(context.filesDir, "scheduled_notifications.pb") }, + ) + private val TAG = "NotificationScheduleManager" + + // Use a coroutine scope for IO operations to avoid blocking the calling thread. + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + private val _scheduledNotifications = MutableStateFlow>(emptyList()) + val scheduledNotifications = _scheduledNotifications.asStateFlow() + + init { + loadNotifications() + } + + fun initialize() = Unit + + /** Loads the scheduled notifications from the disk. */ + private fun loadNotifications() { + coroutineScope.launch { + try { + val file = File(context.filesDir, "scheduled_notifications.pb") + if (file.exists()) { + val data = file.inputStream().use { ScheduledNotificationsSerializer.readFrom(it) } + _scheduledNotifications.value = data.notificationList + } + } catch (e: Exception) { + // Ignore on read fault + } + } + } + + /** Saves the scheduled notifications to the disk. */ + private fun saveNotifications() { + coroutineScope.launch { + dataStore.updateData { + ScheduledNotifications.newBuilder() + .addAllNotification(_scheduledNotifications.value) + .build() + } + } + } + + /** + * Schedules a notification and returns true if the notification was scheduled successfully, + * otherwise returns false. + */ + fun scheduleNotification(notification: ScheduledNotification): Boolean { + if (!setAlarmForNotification(notification)) { + return false + } + _scheduledNotifications.update { it + notification } + saveNotifications() + return true + } + + /** + * Sets an alarm for a notification. If the notification has a date, the alarm is set for the + * specified date and time. Otherwise, the alarm is set for the specified time on the current day. + * If the specified time is in the past, the alarm is set for the specified time on the next day. + * If repeatDaily is true, the alarm is set to repeat daily. + * + * @param notification The notification to schedule. + * @return True if the notification was scheduled successfully, otherwise false. + */ + private fun setAlarmForNotification(notification: ScheduledNotification): Boolean { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + // On Android S (API 31) and later, we must check whether we have permission to set exact + // alarms. If we don't have permission, don't schedule alarms for notifications. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (!alarmManager.canScheduleExactAlarms()) { + // TODO: Request permission from user. + Log.e(TAG, "Failed to schedule notification: permission not granted") + return false + } + } + + val pendingIntent = + NotificationPendingIntentHelper.buildNotificationPendingIntent( + context, + notification.id, + notification.title, + notification.message, + notification.deeplink, + notification.repeatDaily, + notification.hour, + notification.minute, + notification.channelId, + notification.channelName, + ) + + val calendar = + Calendar.getInstance().apply { + timeInMillis = System.currentTimeMillis() + if (notification.hasYear() && notification.hasMonth() && notification.hasDay()) { + set(Calendar.YEAR, notification.year) + set(Calendar.MONTH, notification.month - 1) + set(Calendar.DAY_OF_MONTH, notification.day) + } + set(Calendar.HOUR_OF_DAY, notification.hour) + set(Calendar.MINUTE, notification.minute) + set(Calendar.SECOND, 0) + if (before(Calendar.getInstance())) { + if ( + notification.repeatDaily || + (!notification.hasYear() && !notification.hasMonth() && !notification.hasDay()) + ) { + add(Calendar.DATE, 1) + } + } + } + + if (notification.repeatDaily) { + alarmManager.setRepeating( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + AlarmManager.INTERVAL_DAY, + pendingIntent, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent, + ) + } else { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) + } + return true + } + + /** Reschedules all loaded notifications. Typically called after device boot. */ + fun rescheduleAllNotifications() { + coroutineScope.launch { + try { + val file = File(context.filesDir, "scheduled_notifications.pb") + if (file.exists()) { + val data = file.inputStream().use { ScheduledNotificationsSerializer.readFrom(it) } + for (notification in data.notificationList) { + val unused = setAlarmForNotification(notification) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to reschedule notifications", e) + } + } + } + + /** Removes a notification from the schedule and cancels the alarm for the notification. */ + fun removeNotification(id: String) { + val removed = _scheduledNotifications.value.find { it.id == id } + removed?.let { + val pendingIntent = + NotificationPendingIntentHelper.buildNotificationPendingIntent( + context, + it.id, + it.title, + it.message, + it.deeplink, + it.repeatDaily, + it.hour, + it.minute, + it.channelId, + it.channelName, + ) + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.cancel(pendingIntent) + } + _scheduledNotifications.update { list -> list.filter { it.id != id } } + saveNotifications() + } +} + +object ScheduledNotificationsSerializer : Serializer { + override val defaultValue: ScheduledNotifications = ScheduledNotifications.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): ScheduledNotifications { + try { + return ScheduledNotifications.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: ScheduledNotifications, output: OutputStream) = t.writeTo(output) +} + +@dagger.hilt.EntryPoint +@dagger.hilt.InstallIn(dagger.hilt.components.SingletonComponent::class) +interface NotificationScheduleManagerEntryPoint { + fun notificationScheduleManager(): NotificationScheduleManager +} diff --git a/Android/src/app/src/main/proto/scheduled_notification.proto b/Android/src/app/src/main/proto/scheduled_notification.proto new file mode 100644 index 000000000..e155fa96c --- /dev/null +++ b/Android/src/app/src/main/proto/scheduled_notification.proto @@ -0,0 +1,53 @@ +/* Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.google.ai.edge.gallery.proto; + +option java_package = "com.google.ai.edge.gallery.proto"; +option java_outer_classname = "ScheduledNotificationOuterClass"; +option java_multiple_files = true; + +// This is a single notification that is scheduled to be sent at a specific time +// and date, and may repeat daily. +// Next ID: 13 +message ScheduledNotification { + // The unique identifier of the notification. + string id = 1; + // The title of the notification. + string title = 2; + // The message of the notification. + string message = 3; + // The channel ID of the notification. + string channel_id = 4; + // The channel name of the notification. + string channel_name = 5; + int32 hour = 6; + int32 minute = 7; + optional int32 year = 8; + optional int32 month = 9; + optional int32 day = 10; + // Whether the notification should repeat daily. + optional bool repeat_daily = 11; + // The deeplink to open when the notification is tapped. + optional string deeplink = 12; +} + +// This is a list of all scheduled notifications. +// Next ID: 2 +message ScheduledNotifications { + repeated ScheduledNotification notification = 1; +}