This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

OTA DFU in background with no notification?

Hi, I have implemented OTA DFU on Android with your official DFU library. We want the DFU to happen in background at a certain trigger (car driving, triggered from device = user will stay near device) and without bothering the user that this is happening. My implementation was all good and working fine when the app was in the foreground while this was happening, but since we don't expect the user to be staring at his phone while driving - this needs to happen in the background of course. Problem is, when I tested this the following happend:

SDK 15.3 softdevice s132.

starter created
controller initiated
listener registred
onDfuProcessStarting FA:11:1B:B1:D7:C4
onEnablingDfuMode
onDeviceConnecting
DfuBaseService: Connection state change error: 133 newState: 0
DfuBaseService: Device not reachable. Check if the device with address FA:11:1B:B1:D7:C4 is in range, is advertising and is connectable
DfuBaseService: Connection state change error: 133 newState: 0
DfuBaseService: Device not reachable. Check if the device with address FA:11:1B:B1:D7:C4 is in range, is advertising and is connectable
DfuBaseService: Connection state change error: 133 newState: 0
DfuBaseService: Device not reachable. Check if the device with address FA:11:1B:B1:D7:C4 is in range, is advertising and is

onError 133 type 1 message GATT ERROR

It is implemented in the following way.

a Presenter class contains the function scheduleDFU

private fun scheduleDFU() {
        val fwFile = File(context.getExternalFilesDir(null).toString() + File.separator.toString() + "fw.zip")
        Log.d("DFUTEST","scheduleDFU with " + fwFile.absolutePath + " " + fwFile.absolutePath)

        val starter = deviceComm.getConnectedDevice()?.address?.let {
            DfuServiceInitiator(it)
                .setDeviceName(deviceComm.getConnectedDevice()?.name)
                .setKeepBond(true) // keep bond after dfu
                .setForceDfu(true)
                .setPacketsReceiptNotificationsEnabled(false)
                .setDisableNotification(true) // no foreground notification
                .setForeground(false)
                .setPrepareDataObjectDelay(400L)
        }

        starter!!.setZip(fwFile.toUri(), fwFile.absolutePath)
        Log.d("DFUTEST","starter created")
        
        dfuServiceController = starter!!.start(context, DfuService::class.java)
        Log.d("DFUTEST","controller initiated")
        
        DfuServiceListenerHelper.registerProgressListener(context, dfuProgressListener)
        Log.d("DFUTEST","listener registred")

    }

I didn't want any notifications, and disabled that as you can see above and so the implementation of DfuBaseService (as pr your examples) is very bare bones:

class DfuService : DfuBaseService() {
    @Override
    protected override fun getNotificationTarget(): Class<out Activity> {
        Log.d("DFUTEST","getNotificationTarget")
        return NotificationActivity::class.java
    }

    @Override
    protected override fun isDebug(): Boolean {
        return super.isDebug()
    }

    @Override
    protected override fun updateForegroundNotification(builder: NotificationCompat.Builder) {
        Log.d("DFUTEST","updateForegroundNot")
        super.updateForegroundNotification(builder)
    }
}

The progress listener is also pretty simple, all it does is log and unsubscribe itself

private val dfuProgressListener: DfuProgressListener = object : DfuProgressListenerAdapter() {
        override fun onDeviceConnecting(deviceAddress: String) {
            Log.d("DFUTEST","onDeviceConnecting")
        }

        override fun onDfuProcessStarting(deviceAddress: String) {
            App.dfuInProgress = true
            Log.d("DFUTEST","onDfuProcessStarting " + deviceAddress )
        }

        override fun onEnablingDfuMode(deviceAddress: String) {
            Log.d("DFUTEST","onEnablingDfuMode")
        }

        override fun onFirmwareValidating(deviceAddress: String) {
            Log.d("DFUTEST","onFirmwareValidating")
        }

        override fun onDeviceDisconnecting(deviceAddress: String) {
            Log.d("DFUTEST","onDeviceDisconnecting " + deviceAddress)
            //DfuServiceListenerHelper.unregisterProgressListener(context, this)
        }

        override fun onDfuCompleted(deviceAddress: String) {
            Log.d("DFUTEST","onDfuCompleted")
            App.dfuInProgress = false
            DfuServiceListenerHelper.unregisterProgressListener(context, this)
        }

        override fun onDfuAborted(deviceAddress: String) {
            Log.d("DFUTEST","onDfuAborted")
            DfuServiceListenerHelper.unregisterProgressListener(context, this)
        }
        override fun onProgressChanged(
            deviceAddress: String,
            percent: Int,
            speed: Float,
            avgSpeed: Float,
            currentPart: Int,
            partsTotal: Int
        ) {
            Log.d("DFUTEST","onProgressChanged / " + percent)
        }
        override fun onError(deviceAddress: String, error: Int, errorType: Int, message: String) {
            Log.d("DFUTEST","onError " + error + " type " + errorType + " message " + message)
        }
    }

So my question:

I assume this has to do with me disabling the foreground notification when setting up the DfuServiceInitiator, but I was under the impression that it was possible to do this without a notification?

Best

Søren

Parents
  •  what version of Android are you experiencing this? This may also happen due to the background execution limits as Android has become much more strict on background services and especially when your app is not in foreground. You may refer to the documentation on background services here. As a mobile app developer myself and a IMHO it's always good for the user to know what's going on even though the user may not actively look at the screen, but after all it's your call Slight smile

  • Hi Roshan, 

    I have seen this behavior on Android 8 and 9. How can I circumvent this? Should I put the actual DFU code in a foreground service? Is that recommended? I mean I already have a foreground service for my app, but I didn't put the DFU code there

    EDIT: as far as I can see, it should be automatically started as a foregroundService by your library if Android OS is 8 or newer. Here from DfuServiceInitiator on your github:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && startAsForegroundService) {
    			// On Android Oreo and above the service must be started as a foreground service to make it accessible from
    			// a killed application.
    			context.startForegroundService(intent);
    		} else {
    			context.startService(intent);
    		}
    		return new DfuServiceController(context);

  • This is the default behavior from Android 8  onwards as stated in the documentation. Android has become more strict on background services which is understandable. You can have multiple foreground services so I recommend start the dfu in its own foreground service which it should do it by self. Make sure to add the foreground service permission in the manifest file and follow the usage instructions on Github

  • I have now set the "setForeground(true)" instead of false, and now it crashes the whole app instead:

    2020-11-24 13:05:47.869 10096-10096/"****** E/"******: [qarth_debug:]  get PatchStore::createDisableExceptionQarthFile method fail.
        
        --------- beginning of crash
    2020-11-24 13:05:47.875 10096-10096/"****** E/AndroidRuntime: FATAL EXCEPTION: main
        Process: "******, PID: 10096
        android.app.RemoteServiceException: Bad notification for startForeground: java.lang.RuntimeException: invalid channel for service notification: Notification(channel=dfu pri=-1 contentView=null vibrate=null sound=null defaults=0x0 flags=0x42 color=0xff888888 vis=PRIVATE)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2126)
            at android.os.Handler.dispatchMessage(Handler.java:112)
            at android.os.Looper.loop(Looper.java:216)
            at android.app.ActivityThread.main(ActivityThread.java:7625)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)

    I have the service permission in the manifest, and also declared the service 

    <service
                android:name="******.device.DfuService"
                android:enabled="true"
                android:exported="false"
                android:foregroundServiceType="connectedDevice"
            />

  • This is related to displaying a notification, Did you create a notification channel?

    DfuServiceInitiator.createDfuNotificationChannel(context);

    Please refer to the nRF Toolbox example where you'll find a complete example of the using the DFU library..

Reply Children
  • Yes, it has to do with the notification channel. But I thought the DFU library was handling that already? inside DfuBaseService class, there is this code

    private void startForeground() {
    		final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_DFU)
    				.setSmallIcon(android.R.drawable.stat_sys_upload)
    				.setContentTitle(getString(R.string.dfu_status_foreground_title)).setContentText(getString(R.string.dfu_status_foreground_content))
    				.setColor(Color.GRAY)
    				.setPriority(NotificationCompat.PRIORITY_LOW)
    				.setOngoing(true);
    
    		// Update the notification
    		final Class<? extends Activity> clazz = getNotificationTarget();
    		if (clazz != null) {
    			final Intent targetIntent = new Intent(this, clazz);
    			targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    			targetIntent.putExtra(EXTRA_DEVICE_ADDRESS, mDeviceAddress);
    			targetIntent.putExtra(EXTRA_DEVICE_NAME, mDeviceName);
    			final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    			builder.setContentIntent(pendingIntent);
    		} else {
    			logw("getNotificationTarget() should not return null if the service is to be started as a foreground service");
    			// otherwise the notification will not be clickable.
    		}
    
    		// Any additional configuration?
    		updateForegroundNotification(builder);
    
    		startForeground(NOTIFICATION_ID, builder.build());
    	}
    so the notification id and channel is already defined and should be started from the library itself, no?

  • also, DfuServiceInitiator doesn't appear to know the reference to any method called createDfuNotificationChannel ?

  • Hi it's all done for you and it's just a matter of calling that function. You may notice that it's a static function and you should be able to call it from anywhere and call it before you start the service. There is some more information about it at the end of the documentation.

    Hope this helps

Related