2

The problem

I wrote a program for Android 4.3, with a main activity, a broadcast receiver and a service. The activity binds to the service in its onCreate method. The activity has a button that schedules an alarm 10 seconds in the future. The alarm triggers the BroadcastReceiver.onReceive. This method attempts to get a hold on the binder, but there is a circumstance in which this fails and peekService returns null.

What works

  1. Clicking the button and waiting 10 seconds
  2. Clicking the button, clicking home, and wait in the home screen till 10 seconds are elapsed.
  3. Clicking the button, clicking the activity list, close the activity by swiping it to the left, reopen the program and wait till 10 seconds are elapsed (you need to be fast :-).

What doesn't work

  1. Clicking the button, clicking the activity list, close the activity by swiping it to the left and wait till 10 seconds are elapsed; this is essentially like (3.) without reopening the program.

Specifically, if I execute these 4 tests I get the following log:

02-05 20:53:29.992: D/ServiceSSCCE.MyService(476): I've been bound.
02-05 20:53:30.179: D/ServiceSSCCE.MainActivity(476): Service connected.
02-05 20:53:43.265: D/ServiceSSCCE.MyReceiver(476): Awesome, let's get this **** done!
02-05 20:53:55.460: D/ServiceSSCCE.MyReceiver(476): Awesome, let's get this **** done!
02-05 20:54:08.531: D/ServiceSSCCE.MyService(764): I've been bound.
02-05 20:54:08.663: D/ServiceSSCCE.MainActivity(764): Service connected.
02-05 20:54:10.890: D/ServiceSSCCE.MyReceiver(764): Awesome, let's get this **** done!
02-05 20:54:23.593: D/ServiceSSCCE.MyReceiver(788): I just received a null binder.

The last message shows that test (4.) failed, while previous messages show that (1.), (2.) and (3.) succedeed.

I'm aware of this, this and this answer, as well as pretty much any relevant result that Google lists in the first two pages. I tried several things, including but not limited to:

  • Calling startService, both from BroadcastReceiver.onReceive and from the main activity (although I'm not sure I understood how bindService and startService interact)
  • Fiddling with intents, in particular with context (this VS getApplicationContext and so on).
  • Setting the service as foreground (see this)

I'm really interested in why this happens, more than I'm interested in the solution.

MainActivity.java

package it.damix.examples.servicesscce;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;

public class MainActivity extends Activity {

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d("ServiceSSCCE.MainActivity", "Service disconnected.");
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d("ServiceSSCCE.MainActivity", "Service connected.");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindService(new Intent(this, MyService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    public void scheduleAlarm(View view) {
        AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
        PendingIntent pi = PendingIntent.getBroadcast(this, 101, new Intent(this, MyReceiver.class), 0);
        am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, pi);
    }
}

MyReceiver.java

package it.damix.examples.servicesscce;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        IBinder binder = peekService(context, new Intent(context, MyService.class));

        if (binder == null)
            Log.d("ServiceSSCCE.MyReceiver", "I just received a null binder.");
        else
            Log.d("ServiceSSCCE.MyReceiver", "Awesome, let's get this **** done!");
    }
}

MyService.java

package it.damix.examples.servicesscce;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Messenger;
import android.util.Log;


public class MyService extends Service {

    static class MyHandler extends Handler {
    }

    @Override
    public IBinder onBind(Intent arg0) {
        Log.d("ServiceSSCCE.MyService", "I've been bound.");
        return new Messenger(new MyHandler()).getBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d("ServiceSSCCE.MyService", "I've been unbound.");
        return super.onUnbind(intent);
    }

    @Override
    public void onRebind(Intent intent) {
        Log.d("ServiceSSCCE.MyService", "I've been rebound.");
        super.onRebind(intent);
    }
}

ApplicationManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="it.damix.examples.servicesscce"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="18"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver android:name="it.damix.examples.servicesscce.MyReceiver" android:enabled="true"></receiver>
        <service android:name="it.damix.examples.servicesscce.MyService" android:enabled="true"></service>
    </application>

</manifest>

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="it.damix.examples.servicesscce.MainActivity" >

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click me to schedule an alarm..."
        android:onClick="scheduleAlarm"/>

</RelativeLayout>

Links

Community
  • 1
  • 1
damix911
  • 4,165
  • 1
  • 29
  • 44

2 Answers2

1

+1 for damix911's very complete statement of the problem and steps for possible resolution. The posting of all the code allowed me to easily recreate the test app.

The short answer to the question of why case #4 fails lies in some undocumented details of peekService()'s behavior. In this post by Android framework engineer Dianne Hackborn, she explains that for peekService() to return an IBinder, some component must have previously bound to the service, causing the system to create an IBinder. That post is the only place I have found those additional conditions for obtaining an IBinder described.

Here, for cases #1 through #3, an instance of MainActivity exists and has bound to the service, creating an IBinder. When the receiver runs and calls peekService(), it obtains that IBinder. For case #4, swiping the app from the recent task list kills the entire app process: the activity and bound service. When the alarm subsequently fires, the app process is recreated for the receiver, but the activity is not launched, there is no request to bind to the service, and the service is not created, so peekService() returns null.

I was not able to reproduce damix911's solution. When I modified the service attributes to make it run as an isolatedProcess I got a security exception (KitKat device). I question how starting the service and making it a foreground service would change anything. Starting it and returning the default (super) mode does cause the service to become sticky, so that when the process is recreated after the swipe the service will be recreated. But there is still nothing binding to it, so there is no IBinder for peekService() to return.

Bob Snyder
  • 37,759
  • 6
  • 111
  • 158
0

In short

The solution involves three things:

  • Making the service a foreground service.
  • Starting it in the activity using startService and binding to it. Aparently it's not enough for peekService to having started a service; aparently someone (an activity or probably another service) must have bound to it. Why this is the case it's beyond comprehension (to me).
  • Adding android:isolatedService="true" in the service declaration.

In detail

MyReceiver.java is correct.

The service declared in MyService.java needs indeed to be foreground:

public class MyService extends Service {

    ...

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Notification notification = new Notification.Builder(this)
        .setTicker("Bla bla...")
        .setContentTitle("Title...")
        .setContentText("Text...")
        .build();
        startForeground(1234, notification);

        return super.onStartCommand(intent, flags, startId);
    }

    ...
}

This change in the service caused, at least one time, my activity to raise a connection leak warning (although I was not able to reproduce the behavior). However, it was fixed by properly unbinding the connection; this means removing the service code from onCreate and adding appropriate onResume and onStop methods to MainActivity.java:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

@Override
protected void onResume() {
    startService(new Intent(this, MyService.class));
    bindService(new Intent(this, MyService.class), mConnection, 0);
    super.onResume();
}

@Override
protected void onStop() {
    unbindService(mConnection);
    super.onStop();
}

We are almost done. At this point (1.), (2.) and (3.) still work, and (4.) still doesn't work. The patient, actually got worse: now BroadcastReceive.onReceive doesn't even get triggered.

The final touch that fixes everything is adding the attribute android:isolatedService="true" to the service declaration in ApplicationManifest.xml.

<service
    android:name="it.damix.examples.servicesscce.MyService"
    android:enabled="true"
    android:isolatedProcess="true"></service>
damix911
  • 4,165
  • 1
  • 29
  • 44