Introduce ActiveCallManager to prevent android service crashes during call handling.

This commit is contained in:
Cody Henthorne
2024-01-23 10:34:34 -05:00
committed by Greyson Parrelli
parent ee19520e1b
commit 96823e944d
6 changed files with 443 additions and 26 deletions

View File

@@ -9,6 +9,7 @@ import android.app.Notification
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.core.app.ServiceCompat
import org.signal.core.util.logging.Log
@@ -39,7 +40,7 @@ abstract class SafeForegroundService : Service() {
* @return False if we tried to start the service but failed, otherwise true.
*/
@CheckReturnValue
fun start(context: Context, serviceClass: Class<out SafeForegroundService>): Boolean {
fun start(context: Context, serviceClass: Class<out SafeForegroundService>, extras: Bundle = Bundle.EMPTY): Boolean {
stateLock.withLock {
val state = currentState(serviceClass)
@@ -57,7 +58,10 @@ abstract class SafeForegroundService : Service() {
try {
ForegroundServiceUtil.startWhenCapable(
context = context,
intent = Intent(context, serviceClass).apply { action = ACTION_START }
intent = Intent(context, serviceClass).apply {
action = ACTION_START
putExtras(extras)
}
)
true
} catch (e: UnableToStartException) {
@@ -79,14 +83,15 @@ abstract class SafeForegroundService : Service() {
* Safely stops the service by starting it with an action to stop itself.
* This is done to prevent scenarios where you stop the service while
* a start is pending, preventing the posting of a foreground notification.
* @return true if service was running previously
*/
fun stop(context: Context, serviceClass: Class<out SafeForegroundService>) {
fun stop(context: Context, serviceClass: Class<out SafeForegroundService>): Boolean {
stateLock.withLock {
val state = currentState(serviceClass)
Log.d(TAG, "[stop] Current state: $state")
when (state) {
return when (state) {
State.STARTING -> {
Log.d(TAG, "[stop] Stopping service.")
states[serviceClass] = State.STOPPING
@@ -99,16 +104,19 @@ abstract class SafeForegroundService : Service() {
Log.w(TAG, "Failed to start service class $serviceClass", e)
states[serviceClass] = State.STOPPED
}
true
}
State.STOPPED,
State.STOPPING -> {
Log.d(TAG, "[stop] No need to stop the service. Current state: $state")
false
}
State.NEEDS_RESTART -> {
Log.i(TAG, "[stop] Clearing pending restart.")
states[serviceClass] = State.STOPPING
false
}
}
}

View File

@@ -0,0 +1,330 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc
import android.app.Notification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import androidx.annotation.MainThread
import androidx.core.app.NotificationManagerCompat
import androidx.core.os.bundleOf
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.SafeForegroundService
import org.thoughtcrime.securesms.util.TelephonyUtil
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.Companion.create
import org.thoughtcrime.securesms.webrtc.locks.LockManager
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* Entry point for [SignalCallManager] and friends to interact with the Android system as
* previously done via [WebRtcCallService].
*
* This tries to limit the use of a foreground service until a call has been fully established
* and the user has likely foregrounded us by accepting a call.
*/
class ActiveCallManager(
private val application: Context
) : SignalAudioManager.EventListener {
companion object {
private val TAG = Log.tag(ActiveCallManager::class.java)
}
private val callManager = ApplicationDependencies.getSignalCallManager()
private var networkReceiver: NetworkReceiver? = null
private var powerButtonReceiver: PowerButtonReceiver? = null
private var uncaughtExceptionHandlerManager: UncaughtExceptionHandlerManager? = null
private val webSocketKeepAliveTask: WebSocketKeepAliveTask = WebSocketKeepAliveTask()
private var signalAudioManager: SignalAudioManager? = null
private var previousNotificationId = -1
init {
registerUncaughtExceptionHandler()
registerNetworkReceiver()
webSocketKeepAliveTask.start()
}
fun stop() {
Log.v(TAG, "stop")
uncaughtExceptionHandlerManager?.unregister()
uncaughtExceptionHandlerManager = null
signalAudioManager?.shutdown()
signalAudioManager = null
unregisterNetworkReceiver()
unregisterPowerButtonReceiver()
webSocketKeepAliveTask.stop()
if (!ActiveCallForegroundService.stop(application) && previousNotificationId != -1) {
NotificationManagerCompat.from(application).cancel(previousNotificationId)
}
}
fun update(type: Int, recipientId: RecipientId, isVideoCall: Boolean) {
Log.i(TAG, "update $type $recipientId $isVideoCall")
val notificationId = CallNotificationBuilder.getNotificationId(type)
if (previousNotificationId != notificationId && previousNotificationId != -1) {
NotificationManagerCompat.from(application).cancel(previousNotificationId)
}
previousNotificationId = notificationId
if (type != CallNotificationBuilder.TYPE_ESTABLISHED) {
val notification = CallNotificationBuilder.getCallInProgressNotification(application, type, Recipient.resolved(recipientId), isVideoCall, false)
NotificationManagerCompat.from(application).notify(notificationId, notification)
} else {
ActiveCallForegroundService.start(application, recipientId, isVideoCall)
}
}
fun sendAudioCommand(audioCommand: AudioManagerCommand) {
if (signalAudioManager == null) {
signalAudioManager = create(application, this)
}
Log.i(TAG, "Sending audio command [" + audioCommand.javaClass.simpleName + "] to " + signalAudioManager?.javaClass?.simpleName)
signalAudioManager!!.handleCommand(audioCommand)
}
fun changePowerButton(enabled: Boolean) {
if (enabled) {
registerPowerButtonReceiver()
} else {
unregisterPowerButtonReceiver()
}
}
private fun registerUncaughtExceptionHandler() {
uncaughtExceptionHandlerManager = UncaughtExceptionHandlerManager()
uncaughtExceptionHandlerManager!!.registerHandler(ProximityLockRelease(callManager.lockManager))
}
private fun registerNetworkReceiver() {
if (networkReceiver == null) {
networkReceiver = NetworkReceiver()
application.registerReceiver(networkReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
}
private fun unregisterNetworkReceiver() {
if (networkReceiver != null) {
application.unregisterReceiver(networkReceiver)
networkReceiver = null
}
}
private fun registerPowerButtonReceiver() {
if (!AndroidTelecomUtil.telecomSupported && powerButtonReceiver == null) {
powerButtonReceiver = PowerButtonReceiver()
application.registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
}
}
private fun unregisterPowerButtonReceiver() {
if (powerButtonReceiver != null) {
application.unregisterReceiver(powerButtonReceiver)
powerButtonReceiver = null
}
}
override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set<SignalAudioManager.AudioDevice>) {
callManager.onAudioDeviceChanged(activeDevice, devices)
}
override fun onBluetoothPermissionDenied() {
callManager.onBluetoothPermissionDenied()
}
/** Foreground service started only after a call is established */
class ActiveCallForegroundService : SafeForegroundService() {
companion object {
private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID"
private const val EXTRA_IS_VIDEO_CALL = "IS_VIDEO_CALL"
fun start(context: Context, recipientId: RecipientId, isVideoCall: Boolean) {
val extras = bundleOf(
EXTRA_RECIPIENT_ID to recipientId,
EXTRA_IS_VIDEO_CALL to isVideoCall
)
if (!SafeForegroundService.start(context, ActiveCallForegroundService::class.java, extras)) {
throw UnableToStartException(Exception())
}
}
fun stop(context: Context): Boolean {
return SafeForegroundService.stop(context, ActiveCallForegroundService::class.java)
}
}
override val tag: String
get() = TAG
override val notificationId: Int
get() = CallNotificationBuilder.WEBRTC_NOTIFICATION
private var hangUpRtcOnDeviceCallAnswered: PhoneStateListener? = null
private var notification: Notification? = null
override fun onCreate() {
super.onCreate()
hangUpRtcOnDeviceCallAnswered = HangUpRtcOnPstnCallAnsweredListener()
if (!AndroidTelecomUtil.telecomSupported) {
try {
TelephonyUtil.getManager(application).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
} catch (e: SecurityException) {
Log.w(TAG, "Failed to listen to PSTN call answers!", e)
}
}
}
override fun onDestroy() {
super.onDestroy()
if (!AndroidTelecomUtil.telecomSupported) {
TelephonyUtil.getManager(application).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_NONE)
}
}
override fun getForegroundNotification(intent: Intent): Notification {
if (notification != null) {
return notification!!
} else if (!intent.hasExtra(EXTRA_RECIPIENT_ID)) {
return CallNotificationBuilder.getStoppingNotification(this)
}
val recipientId: RecipientId = intent.getParcelableExtra(EXTRA_RECIPIENT_ID)!!
val isVideoCall = intent.getBooleanExtra(EXTRA_IS_VIDEO_CALL, false)
notification = CallNotificationBuilder.getCallInProgressNotification(
this,
CallNotificationBuilder.TYPE_ESTABLISHED,
Recipient.resolved(recipientId),
isVideoCall,
false
)
return notification!!
}
@Suppress("deprecation")
private class HangUpRtcOnPstnCallAnsweredListener : PhoneStateListener() {
override fun onCallStateChanged(state: Int, phoneNumber: String) {
super.onCallStateChanged(state, phoneNumber)
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
hangup()
Log.i(TAG, "Device phone call ended Signal call.")
}
}
private fun hangup() {
ApplicationDependencies.getSignalCallManager().localHangup()
}
}
}
class ActiveCallServiceReceiver : BroadcastReceiver() {
companion object {
const val ACTION_DENY = "org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"
const val ACTION_HANGUP = "org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.HANGUP"
}
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "action: ${intent?.action}")
when (intent?.action) {
ACTION_DENY -> ApplicationDependencies.getSignalCallManager().denyCall()
ACTION_HANGUP -> ApplicationDependencies.getSignalCallManager().localHangup()
}
}
}
/**
* Periodically request the web socket stay open if we are doing anything call related.
*/
private class WebSocketKeepAliveTask : Runnable {
companion object {
private val REQUEST_WEBSOCKET_STAY_OPEN_DELAY: Duration = 1.minutes
private val WEBSOCKET_KEEP_ALIVE_TOKEN: String = WebSocketKeepAliveTask::class.java.simpleName
}
private var keepRunning = false
@MainThread
fun start() {
if (!keepRunning) {
keepRunning = true
run()
}
}
@MainThread
fun stop() {
keepRunning = false
ThreadUtil.cancelRunnableOnMain(this)
ApplicationDependencies.getIncomingMessageObserver().removeKeepAliveToken(WEBSOCKET_KEEP_ALIVE_TOKEN)
}
@MainThread
override fun run() {
if (keepRunning) {
ApplicationDependencies.getIncomingMessageObserver().registerKeepAliveToken(WEBSOCKET_KEEP_ALIVE_TOKEN)
ThreadUtil.runOnMainDelayed(this, REQUEST_WEBSOCKET_STAY_OPEN_DELAY.inWholeMilliseconds)
}
}
}
private class NetworkReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetworkInfo = connectivityManager.activeNetworkInfo
ApplicationDependencies.getSignalCallManager().apply {
networkChange(activeNetworkInfo != null && activeNetworkInfo.isConnected)
dataModeUpdate()
}
}
}
private class PowerButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_SCREEN_OFF == intent.action) {
ApplicationDependencies.getSignalCallManager().screenOff()
}
}
}
private class ProximityLockRelease(private val lockManager: LockManager) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, throwable: Throwable) {
Log.i(TAG, "Uncaught exception - releasing proximity lock", throwable)
lockManager.updatePhoneState(LockManager.PhoneState.IDLE)
}
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -18,6 +19,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
@@ -25,6 +27,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
@@ -84,7 +87,18 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
private Disposable lastNotificationDisposable = Disposable.disposed();
private boolean stopping = false;
public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId, boolean isVideoCall) {
private static ActiveCallManager activeCallManager = null;
public synchronized static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId, boolean isVideoCall) {
if (FeatureFlags.useActiveCallManager()) {
if (activeCallManager == null) {
activeCallManager = new ActiveCallManager(context);
}
activeCallManager.update(type, recipientId, isVideoCall);
return;
}
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_UPDATE)
.putExtra(EXTRA_UPDATE_TYPE, type)
@@ -95,36 +109,84 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
}
public static void denyCall(@NonNull Context context) {
ForegroundServiceUtil.tryToStartWhenCapable(context, denyCallIntent(context), FOREGROUND_SERVICE_TIMEOUT);
if (FeatureFlags.useActiveCallManager()) {
ApplicationDependencies.getSignalCallManager().denyCall();
return;
}
ForegroundServiceUtil.tryToStartWhenCapable(context, new Intent(context, WebRtcCallService.class).setAction(ACTION_DENY_CALL), FOREGROUND_SERVICE_TIMEOUT);
}
public static void hangup(@NonNull Context context) {
ForegroundServiceUtil.tryToStartWhenCapable(context, hangupIntent(context), FOREGROUND_SERVICE_TIMEOUT);
if (FeatureFlags.useActiveCallManager()) {
ApplicationDependencies.getSignalCallManager().localHangup();
return;
}
ForegroundServiceUtil.tryToStartWhenCapable(context, new Intent(context, WebRtcCallService.class).setAction(ACTION_LOCAL_HANGUP), FOREGROUND_SERVICE_TIMEOUT);
}
public static void stop(@NonNull Context context) {
public synchronized static void stop(@NonNull Context context) {
if (FeatureFlags.useActiveCallManager()) {
if (activeCallManager != null) {
activeCallManager.stop();
activeCallManager = null;
}
return;
}
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_STOP);
ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT);
}
public static @NonNull Intent denyCallIntent(@NonNull Context context) {
return new Intent(context, WebRtcCallService.class).setAction(ACTION_DENY_CALL);
public synchronized static @NonNull PendingIntent denyCallIntent(@NonNull Context context) {
if (FeatureFlags.useActiveCallManager()) {
Intent intent = new Intent(context, ActiveCallManager.ActiveCallServiceReceiver.class);
intent.setAction(ActiveCallManager.ActiveCallServiceReceiver.ACTION_DENY);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntentFlags.mutable());
}
return getServicePendingIntent(context, new Intent(context, WebRtcCallService.class).setAction(ACTION_DENY_CALL));
}
public static @NonNull Intent hangupIntent(@NonNull Context context) {
return new Intent(context, WebRtcCallService.class).setAction(ACTION_LOCAL_HANGUP);
public synchronized static @NonNull PendingIntent hangupIntent(@NonNull Context context) {
if (FeatureFlags.useActiveCallManager()) {
Intent intent = new Intent(context, ActiveCallManager.ActiveCallServiceReceiver.class);
intent.setAction(ActiveCallManager.ActiveCallServiceReceiver.ACTION_HANGUP);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntentFlags.mutable());
}
return getServicePendingIntent(context, new Intent(context, WebRtcCallService.class).setAction(ACTION_LOCAL_HANGUP));
}
public static void sendAudioManagerCommand(@NonNull Context context, @NonNull AudioManagerCommand command) {
public synchronized static void sendAudioManagerCommand(@NonNull Context context, @NonNull AudioManagerCommand command) {
if (FeatureFlags.useActiveCallManager()) {
if (activeCallManager == null) {
activeCallManager = new ActiveCallManager(context);
}
activeCallManager.sendAudioCommand(command);
return;
}
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_SEND_AUDIO_COMMAND)
.putExtra(EXTRA_AUDIO_COMMAND, command);
ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT);
}
public static void changePowerButtonReceiver(@NonNull Context context, boolean register) {
public synchronized static void changePowerButtonReceiver(@NonNull Context context, boolean register) {
if (FeatureFlags.useActiveCallManager()) {
if (activeCallManager == null) {
activeCallManager = new ActiveCallManager(context);
}
activeCallManager.changePowerButton(register);
return;
}
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_CHANGE_POWER_BUTTON)
.putExtra(EXTRA_ENABLED, register);
@@ -337,6 +399,11 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
callManager.onBluetoothPermissionDenied();
}
public static PendingIntent getServicePendingIntent(@NonNull Context context, @NonNull Intent intent) {
return Build.VERSION.SDK_INT >= 26 ? PendingIntent.getForegroundService(context, 0, intent, PendingIntentFlags.mutable())
: PendingIntent.getService(context, 0, intent, PendingIntentFlags.mutable());
}
@SuppressWarnings("deprecation")
private class HangUpRtcOnPstnCallAnsweredListener extends PhoneStateListener {
@Override

View File

@@ -117,7 +117,8 @@ public final class FeatureFlags {
public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
private static final String CALLING_REACTIONS = "android.calling.reactions";
private static final String NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist";
private static final String CALLING_RAISE_HAND = "android.calling.raiseHand";
private static final String CALLING_RAISE_HAND = "android.calling.raiseHand";
private static final String USE_ACTIVE_CALL_MANAGER = "android.calling.useActiveCallManager";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -188,7 +189,8 @@ public final class FeatureFlags {
CALLING_REACTIONS,
NOTIFICATION_THUMBNAIL_BLOCKLIST,
CALLING_RAISE_HAND,
PHONE_NUMBER_PRIVACY
PHONE_NUMBER_PRIVACY,
USE_ACTIVE_CALL_MANAGER
);
@VisibleForTesting
@@ -679,6 +681,11 @@ public final class FeatureFlags {
return getString(NOTIFICATION_THUMBNAIL_BLOCKLIST, "");
}
/** Whether or not to use active call manager instead of WebRtcCallService. */
public static boolean useActiveCallManager() {
return getBoolean(USE_ACTIVE_CALL_MANAGER, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.util.ConversationUtil;
public class CallNotificationBuilder {
private static final int WEBRTC_NOTIFICATION = 313388;
public static final int WEBRTC_NOTIFICATION = 313388;
private static final int WEBRTC_NOTIFICATION_RINGING = 313389;
public static final int TYPE_INCOMING_RINGING = 1;
@@ -114,7 +114,7 @@ public class CallNotificationBuilder {
if (deviceVersionSupportsIncomingCallStyle()) {
builder.setStyle(NotificationCompat.CallStyle.forIncomingCall(
person,
getServicePendingIntent(context, WebRtcCallService.denyCallIntent(context)),
WebRtcCallService.denyCallIntent(context),
getActivityPendingIntent(context, isVideoCall ? LaunchCallScreenIntentState.VIDEO : LaunchCallScreenIntentState.AUDIO)
).setIsVideo(isVideoCall));
}
@@ -138,7 +138,7 @@ public class CallNotificationBuilder {
if (deviceVersionSupportsIncomingCallStyle()) {
builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(
person,
getServicePendingIntent(context, WebRtcCallService.hangupIntent(context))
WebRtcCallService.hangupIntent(context)
).setIsVideo(isVideoCall));
}
@@ -214,13 +214,8 @@ public class CallNotificationBuilder {
}
}
private static PendingIntent getServicePendingIntent(@NonNull Context context, @NonNull Intent intent) {
return Build.VERSION.SDK_INT >= 26 ? PendingIntent.getForegroundService(context, 0, intent, PendingIntentFlags.mutable())
: PendingIntent.getService(context, 0, intent, PendingIntentFlags.mutable());
}
private static NotificationCompat.Action getServiceNotificationAction(Context context, Intent intent, int iconResId, int titleResId) {
return new NotificationCompat.Action(iconResId, context.getString(titleResId), getServicePendingIntent(context, intent));
private static NotificationCompat.Action getServiceNotificationAction(Context context, PendingIntent intent, int iconResId, int titleResId) {
return new NotificationCompat.Action(iconResId, context.getString(titleResId), intent);
}
private static PendingIntent getActivityPendingIntent(@NonNull Context context, @NonNull LaunchCallScreenIntentState launchCallScreenIntentState) {