Reputation: 11642
My App is using new Foreground service design introduced in Android 8.
I have a problem with cancelling notification displayed in the system tray during the service execution. Some services are still causing that notification related to service freezing on the notification bar even if service is not running.
I would like to ask on some recommendations how to do it in the right way because Android Documentation is not clear in this case.
Below is mentioned my approach to execution of service and displaying/ cancelling the notification. Any suggestion is welcome.
Note: I added timeout method to UserManualCheckService to force invocation of the stopSelf() method.
1) Service is started using Worker Manager as an instance of Worker:
List<OneTimeWorkRequest> workRequestList = new ArrayList<>();
OneTimeWorkRequest workRequestUserManual = new OneTimeWorkRequest.Builder(UserManualWorker.class).build();
workRequestList.add(workRequestUserManual);
mWorkManagerInstance.enqueue(workRequestList);
Worker example
public class UserManualWorker extends Worker {
private Context context;
public UserManualWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.context = context;
}
@NonNull
@Override
public Result doWork() {
Intent i = new Intent(context, UserManualCheckService.class);
CommonHelper.runService(context, i);
return Result.SUCCESS;
}
}
2) Service example
Service is downloading some data using HTTP request. Error and Success state of download is ended by using stopSelf() method which should trigger onDestroy() event in parent BaseIntentService service.
public class UserManualCheckService extends BaseIntentService implements HttpResponseListener {
private Context context = null;
@Override
public void onCreate() {
super.onCreate();
}
/**
* Creates an IntentService. Invoked by your subclass's constructor.
*/
public UserManualCheckService() {
super(UserManualCheckService.class.getName());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try { context = this;
//IF DEVICE IS ONLINE FETCH MESSAGES FROM REMOTE SERVER
if (CommonHelper.isConnectedToInternet(context)) {
Logger.i("UserManualCheckService started");
CommonHelper.showServiceNotification(this);
this.getRemoteData();
this.startTimeoutForRequest();
} else {
Logger.i("No need to sync UserManualCheckService now");
stopSelf();
}
} catch (Exception e) {
TrackingExceptionHelper.logException(e);
stopSelf();
}
return START_STICKY_COMPATIBILITY;
}
@Override
protected void onHandleIntent(Intent intent) {
}
private void getRemoteData() throws Exception {
JsonRequest request = HttpHelper.createRequest(Constants.Request.JSON_OBJECT, Request.Method.GET,
Constants.URL.API_URL_BASE_SCHEME, Constants.URL.API_URL_MEDIA_CHECK_MANUAL,
null, this, Request.Priority.HIGH);
HttpHelper.makeRequest(request, true);
}
@Override
public void onError(VolleyError error) {
TrackingExceptionHelper.logException(error);
stopSelf();
}
@Override
public void onResponse(Object response) throws JSONException {
Logger.d("onResponse");
if (response instanceof JSONObject) {
final JSONObject resp = (JSONObject) response;
new Thread(new Runnable() {
@Override
public void run() {
try {
checkLastSavedVersion(resp);
} catch (Exception e) {
TrackingExceptionHelper.logException(e);
}
}
}).start();
stopSelf();
} else {
Logger.e("Parsing Response data as JSON object is not implemented");
}
}
private void checkLastSavedVersion(JSONObject mediaResource) {
try {
Integer serverManualSize = mediaResource.getInt(Constants.Global.KEY_MANUAL_FILE_SIZE);
String localManualSizeAsString = SharedPrefsHelper.getInstance(context).readString(Constants.Global.KEY_MANUAL_FILE_SIZE);
Integer localManualSize = localManualSizeAsString == null ? 0 : Integer.parseInt(localManualSizeAsString);
if(!serverManualSize.equals(localManualSize)) {
new DownloadUserManualAsyncTask(serverManualSize, context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
Logger.i("User manual already downloaded and up-to date");
}
} catch (Exception e) {
TrackingExceptionHelper.logException(e);
} finally {
stopSelf();
}
}
private void startTimeoutForRequest() {
new android.os.Handler().postDelayed(
new Runnable() {
public void run() {
stopSelf();
}
},
10000);
}
}
BaseIntentService
Parent service for all background services. Calling stopSelf() on children is passed to parent and catched in onDestroy() where is service stopped and notification SHOULD be each time canceled.
public abstract class BaseIntentService extends IntentService {
Context context = null;
@Override
public void onCreate() {
super.onCreate();
this.context = this;
}
public BaseIntentService(String name) {
super(name);
}
@Override
public void onDestroy() {
super.onDestroy();
Logger.d("service done, hiding system tray notification");
CommonHelper.stopService(context, this);
NotificationHelper.cancelNotification(Constants.Notification.SERVICE_NOTIFICATION_ID, context);
}
}
Starting execution of the Foreground Service using the helper class:
public static void runService(Context context, Intent i) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, i);
} else {
context.startService(i);
}
}
Displaying notification:
public static void addServiceNotification(Service context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
setupNotificationChannelsLowImportance(notificationManager);
}
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(context, ANDROID_CHANNEL_ID_SYNC);
Notification notification = mBuilder
.setOngoing(false) //Always true in start foreground
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_sync)
.setContentTitle(context.getClass().getSimpleName())
//.setContentTitle(context.getString(R.string.background_sync_is_active))
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setCategory(Notification.CATEGORY_SERVICE)
.build();
notification.flags |=Notification.FLAG_AUTO_CANCEL;
context.startForeground(Constants.Notification.SERVICE_NOTIFICATION_ID, notification);
if(notificationManager != null) {
//notificationManager.notify(Constants.Notification.SERVICE_NOTIFICATION_ID, notification);
}
}
}
Stopping service is done in this way:
public static void stopService(Context context, Service s) {
Intent i = new Intent(context, s.getClass());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
s.stopForeground(Service.STOP_FOREGROUND_DETACH);
s.stopForeground(Service.STOP_FOREGROUND_REMOVE);
} else {
s.stopForeground(true);
}
context.stopService(i);
}
Cancelling notification method called from BaseIntentService onDestroy()
public static void cancelNotification(int notificationId, Context context) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.cancel(notificationId);
}
}
Notation of the service in the AndroidManifest.xml
<!-- [START USER MANUAL CHECK SERVICE] -->
<service
android:name=".service.UserManualCheckService"
android:enabled="true"
android:exported="false"/>
<!-- [END USER MANUAL SERVICE] -->
Upvotes: 2
Views: 4712
Reputation: 5042
The two separate calls to stopForeground(int)
are causing this behavior. stopForeground(int)
takes a bitwise combination of flags, and should not be called twice in a row (because the first call will cause your service to stop being a foreground service, meaning it is no longer appropriate to use stopForeground()
to communicate with it). I'm not even sure what the documented behavior is in this case.
SOLUTION
Simply call stopForeground(true)
regardless of OS version.
Upvotes: 3