From 63791b9f5e6ab25affdd4d3e9df674e2cfab9e9a Mon Sep 17 00:00:00 2001 From: avi Date: Fri, 7 Aug 2026 13:31:29 -0500 Subject: [PATCH] feat: configurable ring settings screen Adds a Settings screen (SettingsActivity) where the user configures: - the SMS trigger phrase (replaces hardcoded RING_NOW/PLAY_SONG) - the sound: system ringtone (with picker) or a picked audio file - the play duration (5-300s, replaces the fixed 30s) Values persist via RingConfig (SharedPreferences). SmsReceiver matches the configured phrase; RingService plays the stored Uri for the configured length. Removes the old MediaStore by-name song search and its hardcoded duration. --- README.md | 26 ++- app/src/main/AndroidManifest.xml | 7 + .../com/example/lostmyphone/MainActivity.kt | 11 +- .../com/example/lostmyphone/RingConfig.kt | 102 +++++++++ .../com/example/lostmyphone/RingService.kt | 110 ++-------- .../example/lostmyphone/SettingsActivity.kt | 199 ++++++++++++++++++ .../com/example/lostmyphone/SmsReceiver.kt | 44 ++-- app/src/main/res/layout/activity_main.xml | 9 +- app/src/main/res/layout/activity_settings.xml | 149 +++++++++++++ app/src/main/res/values/strings.xml | 37 +++- 10 files changed, 560 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/com/example/lostmyphone/RingConfig.kt create mode 100644 app/src/main/java/com/example/lostmyphone/SettingsActivity.kt create mode 100644 app/src/main/res/layout/activity_settings.xml diff --git a/README.md b/README.md index 6ac80b3..5b4e4b4 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,26 @@ An Android app that listens for SMS commands to trigger a loud ring — overridi --- -## SMS Trigger Commands +## SMS Trigger -| Command | Behaviour | -|--------------------|----------------------------------------------------------------------------------| -| `RING_NOW` | Plays the default system ringtone at max volume. | -| `PLAY_SONG ` | Searches local `MediaStore` for a matching song. Plays it, or falls back to the default ringtone if not found. | +The app listens for a **configurable trigger phrase**. When an incoming SMS body +matches it exactly (case-insensitive, trimmed), the app rings. -Playback always runs for exactly **30 seconds**, then the previous volume is -restored. +> Both the phrase, the sound it plays, and the duration are configurable from the +> in-app **Ring Settings** screen (launched from the **"Ring Settings"** button). +> Send the configured phrase to your phone from any other phone to trigger it. -> Send the SMS to your phone from any other phone. The command is matched -> case-insensitively. +### Trigger Phrase (default: `RING_NOW`) +Configured in **Settings**. Any SMS whose body equals the phrase triggers the ring. + +### Sound +Choose between: +- **System ringtone** (or pick a specific one), or +- **Custom song** — pick any audio file on the device. + +### Duration +Playback runs for the configured number of seconds (`5–300`, default **30**), then +the previous volume is restored. --- diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 547e5f1..7890807 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,13 @@ + + + = Build.VERSION_CODES.O) { startForegroundService(intent) } else { diff --git a/app/src/main/java/com/example/lostmyphone/RingConfig.kt b/app/src/main/java/com/example/lostmyphone/RingConfig.kt new file mode 100644 index 0000000..7e9aad3 --- /dev/null +++ b/app/src/main/java/com/example/lostmyphone/RingConfig.kt @@ -0,0 +1,102 @@ +package com.example.lostmyphone + +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri +import android.provider.Settings + +/** + * Persisted configuration for how the app reacts to a trigger SMS: + * - the exact text to listen for (case-insensitive), + * - which sound to play (system ringtone vs. a user-picked audio Uri), + * - how long playback should last. + * + * Stored in SharedPreferences. Reads are cheap and safe to call from the + * SmsReceiver on the main thread. + */ +object RingConfig { + + private const val PREFS = "ring_config" + + private const val KEY_TRIGGER = "trigger_text" + private const val KEY_SOUND_MODE = "sound_mode" // "ringtone" | "song" + private const val KEY_RINGTONE_URI = "ringtone_uri" + private const val KEY_SONG_URI = "song_uri" + private const val KEY_DURATION_SEC = "duration_sec" + + const val SOUND_RINGTONE = "ringtone" + const val SOUND_SONG = "song" + + val DEFAULT_TRIGGER: String = "RING_NOW" + const val DEFAULT_DURATION_SEC = 30 + const val MIN_DURATION_SEC = 5 + const val MAX_DURATION_SEC = 300 + + private fun prefs(context: Context): SharedPreferences { + val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + return sp + } + + fun triggerText(context: Context): String = + prefs(context).getString(KEY_TRIGGER, DEFAULT_TRIGGER) ?: DEFAULT_TRIGGER + + fun setTriggerText(context: Context, value: String): Boolean = + prefs(context).edit().putString(KEY_TRIGGER, value.trim()).commit() + + fun soundMode(context: Context): String = + prefs(context).getString(KEY_SOUND_MODE, SOUND_RINGTONE) ?: SOUND_RINGTONE + + fun setSoundMode(context: Context, mode: String): Boolean = + prefs(context).edit().putString(KEY_SOUND_MODE, mode).commit() + + fun songUri(context: Context): Uri? { + val raw = prefs(context).getString(KEY_SONG_URI, null) ?: return null + return try { + Uri.parse(raw) + } catch (e: Exception) { + null + } + } + + fun setSongUri(context: Context, uri: Uri?): Boolean = + prefs(context).edit().putString(KEY_SONG_URI, uri?.toString()).commit() + + fun ringtoneUri(context: Context): Uri? { + val raw = prefs(context).getString(KEY_RINGTONE_URI, null) ?: return null + return try { + Uri.parse(raw) + } catch (e: Exception) { + null + } + } + + fun setRingtoneUri(context: Context, uri: Uri?): Boolean = + prefs(context).edit().putString(KEY_RINGTONE_URI, uri?.toString()).commit() + + fun durationSeconds(context: Context): Int = + prefs(context).getInt(KEY_DURATION_SEC, DEFAULT_DURATION_SEC).coerceIn( + MIN_DURATION_SEC, + MAX_DURATION_SEC + ) + + fun setDurationSeconds(context: Context, seconds: Int): Boolean = + prefs(context).edit().putInt(KEY_DURATION_SEC, seconds.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC)).commit() + + /** + * The Uri the RingService should actually play. + * - ringtone mode -> the system default ringtone, + * - song mode -> the user-picked song, else fall back to the ringtone. + */ + fun playableUri(context: Context): Uri? { + return when (soundMode(context)) { + SOUND_SONG -> songUri(context) ?: Settings.System.DEFAULT_RINGTONE_URI + else -> ringtoneUri(context) ?: Settings.System.DEFAULT_RINGTONE_URI + } + } + + fun isConfiguredTrigger(context: Context, body: String): Boolean { + if (body.isBlank()) return false + // Put the persisted text on the left so it is the caller-provided value. + return body.trim().equals(triggerText(context), ignoreCase = true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/lostmyphone/RingService.kt b/app/src/main/java/com/example/lostmyphone/RingService.kt index 0ee346a..45b46e7 100644 --- a/app/src/main/java/com/example/lostmyphone/RingService.kt +++ b/app/src/main/java/com/example/lostmyphone/RingService.kt @@ -5,7 +5,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import androidx.core.app.NotificationCompat -import android.content.ContentUris import android.content.Context import android.content.Intent import android.media.AudioAttributes @@ -15,7 +14,6 @@ import android.media.MediaPlayer import android.net.Uri import android.os.Build import android.os.IBinder -import android.provider.MediaStore import android.provider.Settings import android.util.Log import kotlinx.coroutines.CoroutineScope @@ -45,22 +43,14 @@ class RingService : Service() { private const val TAG = "RingService" const val ACTION_RING = "com.example.lostmyphone.action.RING" - const val ACTION_PLAY_SONG = "com.example.lostmyphone.action.PLAY_SONG" - const val EXTRA_SONG_NAME = "extra_song_name" const val EXTRA_DND_GRANTED = "extra_dnd_granted" private const val CHANNEL_ID = "ring_channel" private const val NOTIFICATION_ID = 1001 - private const val RING_DURATION_MS = 30_000L - fun buildIntent(context: Context, dndGranted: Boolean, songName: String?): Intent { + fun buildIntent(context: Context, dndGranted: Boolean): Intent { val intent = Intent(context, RingService::class.java) - if (songName.isNullOrBlank()) { - intent.action = ACTION_RING - } else { - intent.action = ACTION_PLAY_SONG - intent.putExtra(EXTRA_SONG_NAME, songName) - } + intent.action = ACTION_RING intent.putExtra(EXTRA_DND_GRANTED, dndGranted) return intent } @@ -110,11 +100,10 @@ class RingService : Service() { } // Explicitly granted: proceed with the ring. - val songName = intent.getStringExtra(EXTRA_SONG_NAME) - Log.i(TAG, "DND access granted. Ringing with song='$songName'") - startForeground(NOTIFICATION_ID, buildRingingNotification(songName).build()) + Log.i(TAG, "DND access granted. Ringing.") + startForeground(NOTIFICATION_ID, buildRingingNotification().build()) - handleRing(songName) + handleRing() return START_STICKY } @@ -122,14 +111,10 @@ class RingService : Service() { // Ring logic // ----------------------------------------------------------------------- - private fun handleRing(songName: String?) { - val uri: Uri? = if (songName.isNullOrBlank()) { - // RING_NOW -> default system ringtone - Settings.System.DEFAULT_RINGTONE_URI - } else { - // PLAY_SONG -> try to find a matching local track, else fallback. - querySongUri(songName) ?: Settings.System.DEFAULT_RINGTONE_URI - } + private fun handleRing() { + // Sound + duration come from the user's settings. + val uri: Uri? = RingConfig.playableUri(this) + val durationMs = RingConfig.durationSeconds(this) * 1_000L if (uri == null) { Log.e(TAG, "No playable URI. Aborting.") @@ -142,8 +127,8 @@ class RingService : Service() { restoreAndStop() return@launch } - // Ring for exactly 30 seconds. - delay(RING_DURATION_MS) + // Ring for the configured duration. + delay(durationMs) restoreAndStop() } } @@ -256,67 +241,6 @@ class RingService : Service() { } } - /** - * Queries MediaStore for a song whose title (or display name) contains the - * requested name, case-insensitively. - */ - private fun querySongUri(songName: String): Uri? { - val needle = songName.trim() - if (needle.isEmpty()) return null - - val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) - } else { - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - } - - val projection = arrayOf( - MediaStore.Audio.Media._ID, - MediaStore.Audio.Media.TITLE, - MediaStore.Audio.Media.DISPLAY_NAME - ) - - val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0" - val selectionArgs: Array? = null - val sortOrder: String? = null - - return try { - val cursor = contentResolver.query( - collection, - projection, - selection, - selectionArgs, - sortOrder - ) ?: return null - - var best: Uri? = null - cursor.use { - if (it.moveToFirst()) { - do { - val title = - it.getString(it.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)) - ?: "" - val display = - it.getString(it.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)) - ?: "" - if (title.contains(needle, ignoreCase = true) || - display.contains(needle, ignoreCase = true) - ) { - val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)) - best = ContentUris.withAppendedId(collection, id) - break - } - } while (it.moveToNext()) - } - } - Log.i(TAG, "Query '$needle' -> ${best?.lastPathSegment ?: "not found"}") - best - } catch (e: Exception) { - Log.e(TAG, "MediaStore query failed", e) - null - } - } - // ----------------------------------------------------------------------- // Cleanup // ----------------------------------------------------------------------- @@ -384,12 +308,12 @@ class RingService : Service() { } } - private fun buildRingingNotification(songName: String?): NotificationCompat.Builder { - val content = if (songName.isNullOrBlank()) { - getString(R.string.notif_ringing_ringtone) - } else { - getString(R.string.notif_ringing_song, songName) - } + private fun buildRingingNotification(): NotificationCompat.Builder { + val content = getString( + R.string.notif_ringing, + RingConfig.durationSeconds(this), + RingConfig.soundMode(this) + ) val openIntent = PendingIntent.getActivity( this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE diff --git a/app/src/main/java/com/example/lostmyphone/SettingsActivity.kt b/app/src/main/java/com/example/lostmyphone/SettingsActivity.kt new file mode 100644 index 0000000..9412087 --- /dev/null +++ b/app/src/main/java/com/example/lostmyphone/SettingsActivity.kt @@ -0,0 +1,199 @@ +package com.example.lostmyphone + +import android.content.Intent +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.OpenableColumns +import android.widget.Button +import android.widget.EditText +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.SeekBar +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import android.media.RingtoneManager + +/** + * Settings screen for the ring behaviour: + * - the SMS trigger phrase, + * - the sound to play (system ringtone or a picked audio file), + * - how long playback lasts. + * + * Values are written to RingConfig only when the user taps Save. + */ +class SettingsActivity : AppCompatActivity() { + + private lateinit var etTrigger: EditText + private lateinit var rgSound: RadioGroup + private lateinit var rdbRingtone: RadioButton + private lateinit var rdbSong: RadioButton + private lateinit var btnPickRingtone: Button + private lateinit var btnPickSong: Button + private lateinit var tvSelectedSound: TextView + private lateinit var sbDuration: SeekBar + private lateinit var tvDurationValue: TextView + private lateinit var btnSave: Button + + private var pickedRingtoneUri: Uri? = null + private var pickedSongUri: Uri? = null + + private val pickRingtoneLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val data = result.data + pickedRingtoneUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) + } else { + @Suppress("DEPRECATION") + data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + } + if (pickedRingtoneUri == null) { + // User may have chosen "silent"/default; treat null as default ringtone. + Toast.makeText(this, R.string.toast_ringtone_default, Toast.LENGTH_SHORT).show() + } + updateSoundSummary() + } + } + + private val pickSongLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + try { + contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + pickedSongUri = uri + updateSoundSummary() + } catch (e: Exception) { + Toast.makeText(this, R.string.toast_song_failed, Toast.LENGTH_LONG).show() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + etTrigger = findViewById(R.id.etTrigger) + rgSound = findViewById(R.id.rgSound) + rdbRingtone = findViewById(R.id.rdbRingtone) + rdbSong = findViewById(R.id.rdbSong) + btnPickRingtone = findViewById(R.id.btnPickRingtone) + btnPickSong = findViewById(R.id.btnPickSong) + tvSelectedSound = findViewById(R.id.tvSelectedSound) + sbDuration = findViewById(R.id.sbDuration) + tvDurationValue = findViewById(R.id.tvDurationValue) + btnSave = findViewById(R.id.btnSave) + + loadCurrentValues() + setupDurationSeekBar() + setupPickers() + + btnSave.setOnClickListener { saveSettings() } + } + + private fun loadCurrentValues() { + etTrigger.setText(RingConfig.triggerText(this)) + + val mode = RingConfig.soundMode(this) + if (mode == RingConfig.SOUND_SONG) rdbSong.isChecked = true else rdbRingtone.isChecked = true + + pickedRingtoneUri = RingConfig.ringtoneUri(this) + pickedSongUri = RingConfig.songUri(this) + + sbDuration.progress = RingConfig.durationSeconds(this) - RingConfig.MIN_DURATION_SEC + updateDurationLabel() + updateSoundSummary() + } + + private fun setupDurationSeekBar() { + val span = RingConfig.MAX_DURATION_SEC - RingConfig.MIN_DURATION_SEC + sbDuration.max = span + sbDuration.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + updateDurationLabel() + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} + }) + } + + private fun updateDurationLabel() { + val seconds = sbDuration.progress + RingConfig.MIN_DURATION_SEC + tvDurationValue.text = getString(R.string.duration_value, seconds) + } + + private fun setupPickers() { + btnPickRingtone.setOnClickListener { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getString(R.string.ringtone_picker_title)) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, pickedRingtoneUri) + } + pickRingtoneLauncher.launch(intent) + } + + btnPickSong.setOnClickListener { + pickSongLauncher.launch(arrayOf("audio/*")) + } + } + + private fun updateSoundSummary() { + val mode = if (rdbSong.isChecked) RingConfig.SOUND_SONG else RingConfig.SOUND_RINGTONE + tvSelectedSound.text = when (mode) { + RingConfig.SOUND_SONG -> { + val uri = pickedSongUri + if (uri == null) getString(R.string.sound_summary_song_none) + else getString(R.string.sound_summary_song, displayNameFor(uri)) + } + else -> { + val uri = pickedRingtoneUri + if (uri == null) getString(R.string.sound_summary_ringtone_default) + else getString(R.string.sound_summary_ringtone, displayNameFor(uri)) + } + } + } + + private fun displayNameFor(uri: Uri): String { + return try { + val cursor: Cursor? = contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (idx >= 0) it.getString(idx)?.let { name -> return name } + } + } + uri.lastPathSegment ?: uri.toString() + } catch (e: Exception) { + uri.toString() + } + } + + private fun saveSettings() { + val trigger = etTrigger.text.toString().trim() + if (trigger.isEmpty()) { + etTrigger.error = getString(R.string.error_trigger_empty) + return + } + + val mode = if (rdbSong.isChecked) RingConfig.SOUND_SONG else RingConfig.SOUND_RINGTONE + val seconds = sbDuration.progress + RingConfig.MIN_DURATION_SEC + + RingConfig.setTriggerText(this, trigger) + RingConfig.setSoundMode(this, mode) + RingConfig.setRingtoneUri(this, pickedRingtoneUri) + RingConfig.setSongUri(this, pickedSongUri) + RingConfig.setDurationSeconds(this, seconds) + + Toast.makeText(this, R.string.toast_saved, Toast.LENGTH_SHORT).show() + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/lostmyphone/SmsReceiver.kt b/app/src/main/java/com/example/lostmyphone/SmsReceiver.kt index d1126d8..a535d0b 100644 --- a/app/src/main/java/com/example/lostmyphone/SmsReceiver.kt +++ b/app/src/main/java/com/example/lostmyphone/SmsReceiver.kt @@ -10,43 +10,33 @@ import androidx.core.content.ContextCompat /** * BroadcastReceiver that intercepts incoming SMS messages and looks for the - * trigger commands: + * user-configured trigger phrase (RingConfig.triggerText, e.g. "RING_NOW"). * - * - RING_NOW -> play the default system ringtone - * - PLAY_SONG -> play a matching local MediaStore song, else fallback + * When a matching message is received, RingService is started as a foreground + * service, which plays the configured sound (Ringtone or a picked song) for the + * configured duration. * * On Android 6.0+ this receiver only fires if the user has granted the - * RECEIVE_SMS runtime permission (which MainActivity requests on first launch). + * RECEIVE_SMS runtime permission (granted via MainActivity/Settings). */ class SmsReceiver : BroadcastReceiver() { - companion object { - private const val TAG = "SmsReceiver" - - val TRIGGER_RING_NOW: Regex = Regex("^\\s*RING_NOW\\s*$", RegexOption.IGNORE_CASE) - val TRIGGER_PLAY_SONG: Regex = - Regex("^\\s*PLAY_SONG\\s+(.+?)\\s*$", RegexOption.IGNORE_CASE) - } - override fun onReceive(context: Context, intent: Intent) { if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { return } - // We only act on a trigger and ignore others, but aborting a broadcast - // that we did not handle would break other apps, so we do NOT call abort. + // We only act on a trigger and ignore others; we do NOT call abort() so + // other applications are not affected by broadcasts we do not handle. val (body, sender) = parseMessages(intent) ?: return Log.i(TAG, "SMS from '$sender': '$body'") - when { - TRIGGER_RING_NOW.matches(body) -> handleTrigger(context, null) - else -> { - TRIGGER_PLAY_SONG.find(body)?.let { match -> - val songName = match.groupValues[1].trim() - handleTrigger(context, songName) - } - } + if (!RingConfig.isConfiguredTrigger(context, body)) { + Log.i(TAG, "Not a trigger. Configured text='${RingConfig.triggerText(context)}'") + return } + + handleTrigger(context, sender) } /** @@ -66,19 +56,23 @@ class SmsReceiver : BroadcastReceiver() { return Pair(body.toString().trim(), sender ?: "unknown") } - private fun handleTrigger(context: Context, songName: String?) { - Log.i(TAG, "Trigger matched. song='$songName'") + private fun handleTrigger(context: Context, sender: String) { + Log.i(TAG, "Trigger matched from '$sender'.") // Determine the current DND access state so the service can short-circuit // if needed. The service re-verifies this at runtime as the source of truth. val dndGranted = (context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager).isNotificationPolicyAccessGranted - val serviceIntent = RingService.buildIntent(context, dndGranted, songName) + val serviceIntent = RingService.buildIntent(context, dndGranted) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ContextCompat.startForegroundService(context, serviceIntent) } else { context.startService(serviceIntent) } } + + companion object { + private const val TAG = "SmsReceiver" + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 04a840a..47c1bef 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -77,10 +77,17 @@ android:text="@string/btn_notifications" />