package com.dcntrlized.alarm import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.view.WindowManager import java.lang.ref.WeakReference import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate /** * The full-screen alarm takeover. * * Launched by the `PendingIntent` of an `AlarmManager.setAlarmClock` entry (see * [AlarmScheduler]), so it appears over the lock screen and turns the screen on * — an alarm launch, not a notification tap. It reuses the main JS bundle and is * pointed at the `LiveAlarm` route via the intent extras, so the takeover is the * same React screen whether the app was foreground, background or dead. * * Manifest requirements that make this work (see AndroidManifest.xml): * `launchMode=singleTask`, `excludeFromRecents=true`, `showWhenLocked` and * `turnScreenOn` on the activity. `USE_FULL_SCREEN_INTENT` is still declared * because it is the permission the platform uses to decide whether this app may * take over the screen at all. */ class AlarmActivity : ReactActivity() { override fun getMainComponentName(): String = "AlarmClient" override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) override fun onCreate(savedInstanceState: Bundle?) { // Order matters: the flags must be set before the window is shown. showOverLockScreen() super.onCreate(savedInstanceState) current = WeakReference(this) } private fun showOverLockScreen() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) setTurnScreenOn(true) } else { @Suppress("DEPRECATION") window.addFlags( WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON ) } window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // Dismiss the keyguard where the OS permits it (a secure lock screen is // still honoured — we never try to bypass a PIN). val km = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && km?.isKeyguardLocked == true) { km.requestDismissKeyguard(this, null) } } /** Hardware back is consumed: the takeover's only exits are in-app actions. */ @Deprecated("Deprecated in Java") override fun onBackPressed() { // Intentionally swallowed — see the Live alarm takeover spec. } override fun onDestroy() { if (current?.get() === this) current = null AlarmPlayer.stop(this) // The ringing notification dies with the ring it announced. NOT in // AlarmPlayer.stop: play() calls that first, which would retract the // full-screen intent that raised this activity. AlarmNotifier.cancelFiring(this) super.onDestroy() } companion object { /** * The takeover that is on screen, if any. * * The JS "Back to alerts" action pops the route but cannot dismiss an * Android activity, so without this the alarm window would stay on top * for good — visible over the lock screen, holding the screen awake, in a * task that is excluded from Recents. That is the reported "no way back to * the main menu". [AlarmModule.finishTakeover] ends it. */ @Volatile private var current: WeakReference? = null fun currentActivity(): AlarmActivity? = current?.get() const val EXTRA_ALERT_ID = "alarm.alertId" const val EXTRA_MESSAGE = "alarm.message" fun intent(context: Context, alertId: String, message: String): Intent = Intent(context, AlarmActivity::class.java).apply { addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS ) putExtra(EXTRA_ALERT_ID, alertId) putExtra(EXTRA_MESSAGE, message) } /** The alert id this launch is for, if any — read by the JS bridge. */ fun alertIdOf(activity: Activity?): String? = activity?.intent?.getStringExtra(EXTRA_ALERT_ID) } }