Search code examples
androidkotlinandroid-activityandroid-serviceandroid-alertdialog

Android 'BadTokenException window token android.os.BinderProxy@4250d6d8 is not valid' with foreground service running


Trying to build an android application using kotlin where I am connecting, sending and reading data from a BLE (Bluetooth Low Energy) device. I have designed an activity which will show me connection stats and data that is being received from BLE device.And I have a foreground service running to keep the connection with the BLE device alive and listen to the stats. I have used pending intent to open this activity from my foreground service notification.

Following code shows the method to create notification

private fun getServiceNotification(textToShowInNotification: String)
{
        val pendingIntent = Intent(<Static context from application class>,ActivityName::class.java)
        pendingIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
        val contentIntent = PendingIntent.getActivity(<static_context_from_application_class>, 0, pendingIntent, 0)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
        {
            val notificationChannel = NotificationChannel("DATA_CHANNEL", <Static context from application class>.resources.getString(R.string.app_name),NotificationManager.IMPORTANCE_DEFAULT)
            notificationManager!!.createNotificationChannel(notificationChannel)
        }

        notification = notificationBuilder!!.setOngoing(true)
            .setOnlyAlertOnce(true)
            .setContentText(textToShowInNotification)
            .setContentIntent(contentIntent)
            .build()
        notificationManager!!.notify(NOTIFICATION_ID, notification)
}

Since I have the use case to open the activity where BLE connections and data receiving is shown. Multiple instances of the activity is created and previous once is not destroyed and because of the same I am unable to get the context of the running activity and due to this I am facing a problem to show alert dialogs in the activity.

Actual Usecase - When the connection with device is established and data is listened from service at that time when I kill the app and open it from foreground service notification, the dialog which has to show up saying that my device is still connected and data is being received is not shown and exceptions occurs when dialog is to be displayed

Following errors/ exceptions occurs when trying to show alert dialogs

D/DialogExecption: Unable to add window -- token null is not valid; is your activity running?
D/DialogExecption: Unable to add window token android.os.BinderProxy@4250d6d8 is not valid; is your activity running?

I have used below code to show the dialog

private fun showAlertDialog()
{
    val dialogAlertDialog = AlertDialog.Builder(<Activity Context>)
    val inflater: LayoutInflater = layoutInflater
    val dialogAlertView: View = inflater.inflate(R.layout.activity_xml_file,null)
    dialogAlertDialog.setView(dialogAlertView)
    dialogAlertDialog.setCancelable(false)

    val builderAlertDialog : AlertDialog = dialogAlertDialog.create()
    try
    {
        builderAlertDialog.show()
    }
    catch(exception: Exception)
    {
        Log.d("DialogExecption",""+exception.message)
    }
}

I have also tried the way

if (!this.isFinishing)
{
    builderAlertDialogCycleCancelled.show()
}

but this too is not helping. Above code will supress the execption but I don't want to do it instead I want to show the dialog at any cost.

To give a pov I have tried the below in my manifest file to keep a single instance of the activity possible but this is not working out.

<activity android:name=".ActivityName"
        android:alwaysRetainTaskState="true"
        android:launchMode="singleTop"
        android:screenOrientation="portrait"
        android:theme="@style/AppThemeGeneral">

</activity>

I have to use alert dialog in the best possible way and skipping it isn't a choice. Any help would be great.

Important Note - I am using Coroutines with scope Dispatchers.IO in my activity to fetch data from the BLE Device.


Solution

  • Observations

        notification = notificationBuilder!!.setOngoing(true)
            .setOnlyAlertOnce(true)
            .setContentText(textToShowInNotification)
            .setContentIntent(contentIntent)
            .build()
        notificationManager!!.notify(NOTIFICATION_ID, notification)
    

    Clicking on this notification may not start an Activity because notification is missing setSmallIcon or similar setIcon call. To fix it, set some icon as below:

         notification = notificationBuilder!!.setOngoing(true)
             .setOnlyAlertOnce(true)
             .setContentText(textToShowInNotification)
             .setContentIntent(contentIntent)
             .setSmallIcon(R.drawable.ic_launcher_background) 
             .build()
         notificationManager!!.notify(NOTIFICATION_ID, notification)
    

    previous activity is not destroyed

    That's doubtful because you're using flags Intent.FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_CLEAR_TASK when starting an Activity,and that will finish all previous Activities. Otherwise, please show the relevant code.

    When the connection with device is established and data is listened from service at that time when I kill the app and open it from foreground service notification, the dialog which has to show up saying that my device is still connected and data is being received is not shown

    This sounds quite confusing. It sounds like you're showing it from your Activity, but then it's unlikely to fail, especially, if you show it from onCreate of your Activity:

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        showAlertDialog()
    }
    

    D/DialogExecption: Unable to add window -- token null is not valid; is your activity running? D/DialogExecption: Unable to add window token android.os.BinderProxy@4250d6d8 is not valid; is your activity running?

    The error message is quite clear - you're trying to show a dialog based on an invalid Context here AlertDialog.Builder(<context>). For instance, if you use applicationContext or a Service context there, it will fail with such exception.

    You claim to start your dialog with an Activity context as below:

    ...
    val dialogAlertDialog = AlertDialog.Builder(<Activity Context>)
    ...
    

    However, it's not clear from your code from where showAlertDialog() is called, and the context object is not shown either. So, I created a sample project to test the described behaviour.

    Suggestion

    Preparation

    I tried reproducing the issue by building a minimalistic project based on the information that you provided in your question. Please note that I'm not using any BLE functionality for this example, even though Bluetooth is used as a prefix for each component.

    I created a foreground service BluetoothDeviceService responsible for starting an Activity, when a notification is clicked.

    BluetoothDeviceService.kt

    class BluetoothDeviceService: Service() {
        private val SERVICE_NOTIFICATION_ID = 123
        private val SERVICE_NOTIFICATION_CHANNEL_ID = "channel_01"
    
        override fun onCreate() {
            super.onCreate()
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(
                    SERVICE_NOTIFICATION_CHANNEL_ID,
                    "Bluetooth service",
                    NotificationManager.IMPORTANCE_DEFAULT)
                val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                notificationManager.createNotificationChannel(channel)
    
                val pendingIntent = Intent()
                pendingIntent.setClass(this,BluetoothDeviceActivity::class.java)
                pendingIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
                val contentIntent = PendingIntent.getActivity(this, 0,
                    pendingIntent, 0)
    
                val notification = NotificationCompat.Builder(this, SERVICE_NOTIFICATION_CHANNEL_ID)
                    .setOnlyAlertOnce(true)
                    .setOngoing(true)
                    .setContentText("Bluetooth service running...")
                    .setContentIntent(contentIntent)
                    .setSmallIcon(R.drawable.ic_launcher_background)
                    .build()
                startForeground(SERVICE_NOTIFICATION_ID, notification)
            }
        }
    
        override fun onBind(intent: Intent?): IBinder? {
            TODO("Not yet implemented")
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            return START_STICKY
        }
    }
    

    I created a MainActivity that had to start the foreground service.

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val startServiceIntent = Intent(this, BluetoothDeviceService::class.java)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(startServiceIntent)
            } else {
                startService(startServiceIntent)
            }
            finish()
        }
    }
    

    Please note that the Service could also be started by BroadcastReceiver, and that would be more appropriate, but I used the Activity for simplicity.

    Also, I introduced a BluetoothDeviceActivity that was started by the service with help of PendingIntent:

    BluetoothDeviceActivity.kt

    class BluetoothDeviceActivity: AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContentView(R.layout.activity_main)
            showAlertDialog()
        }
    }
    
    private fun showAlertDialog() {
        val dialogAlertDialog = AlertDialog.Builder(this)
            .setCancelable(false)
            .setMessage("This is a test")
            .setTitle("Information")
            .create()
    
        dialogAlertDialog.show()
    }
    

    Just in case, I also put my manifest.

    AndroidManifest.xml

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.stackoverflowquestion2">
    
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    
        <application
            android:name=".MainApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.StackOverflowQuestion2"
            android:fullBackupContent="@xml/backup_descriptor">
            <activity
                android:name=".MainActivity"
                android:label="@string/app_name"
                android:theme="@style/Theme.StackOverflowQuestion2.NoActionBar">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
    
            <activity android:name=".BluetoothDeviceActivity"/>
    
            <service android:name=".BluetoothDeviceService"/>
        </application>
    
    </manifest>
    

    Result

    This worked as expected without any problems.

    Further suggestion

    One other idea - you could convert your AlertDialog to Activity and use it as a Dialog. For this, you need to do 2 things:

    1. Create a new Àctivity as below:

      class AlertDialogActivity: AppCompatActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
      
              val dialogAlertDialog = AlertDialog.Builder(this)
                  .setCancelable(false)
                  .setMessage("This is a test")
                  .setTitle("Information")
                  .setPositiveButton("OK") { dialog, which -> finish() }
                  .create()
      
              dialogAlertDialog.show()
          }
      }
      
    2. Add it to your manifest and set its theme as Dialog:

      <activity android:name=".AlertDialogActivity" android:theme="@style/Theme.AppCompat.Dialog"/>
      
    3. Then, create a method in your Service, and use it any time it's needed:

      private fun showAlertDialog() {
          val intent = Intent(applicationContext, AlertDialogActivity::class.java)
          intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
          applicationContext.startActivity(intent)
      }
      

    Result

    That's what it looks like:

    enter image description here