Как правильно отменить уведомление о завершении службы переднего плана в Android 8

Мое приложение использует новый дизайн сервисов Foreground, представленный в Android 8.

У меня проблема с отменой отображения уведомление в системном трее во время выполнения службы. Некоторые службы по-прежнему вызывают это уведомление, связанное с зависанием службы, на панели уведомлений, даже если служба не запущена.

Я хотел бы спросить несколько рекомендаций, как это сделать правильно, потому что документация Android в этом случае не ясна.

Ниже описан мой подход к выполнению услуги и отображению / отмене уведомления. Любые предложения приветствуются.

Примечание: я добавил метод тайм-аута в UserManualCheckService, чтобы принудительно вызвать метод stopSelf ().

1) Служба запускается с использованием Рабочий менеджер в качестве экземпляра Worker:

List<OneTimeWorkRequest> workRequestList = new ArrayList<>();
    OneTimeWorkRequest workRequestUserManual = new OneTimeWorkRequest.Builder(UserManualWorker.class).build();
workRequestList.add(workRequestUserManual);
mWorkManagerInstance.enqueue(workRequestList);

Пример рабочего

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) Пример обслуживания

Сервис загружает некоторые данные с помощью HTTP-запроса. Состояние ошибки и успешной загрузки завершается использованием stopSelf () метод, которое должно запускать событие onDestroy () в родительской службе BaseIntentService.

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

Родительский сервис для всех фоновых сервисов. Вызов stopSelf () для дочерних элементов передается родительскому элементу и перехватывается в onDestroy (), где служба остановлена, и уведомление СЛЕДУЕТ каждый раз отменять.

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);
    }
}

Запуск выполнения службы переднего плана с помощью вспомогательного класса:

  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);
        }
    }

Отображение уведомления:

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);
        }
    }
}

Остановка сервиса производится таким образом:

   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);
    }

Отмена метода уведомления, вызванного из BaseIntentService onDestroy ()

   public static void cancelNotification(int notificationId, Context context) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        if (notificationManager != null) {
            notificationManager.cancel(notificationId);
        }
    }

Обозначение службы в AndroidManifest.xml

 <!-- [START USER MANUAL CHECK SERVICE] -->
        <service
            android:name = ".service.UserManualCheckService"
            android:enabled = "true"
            android:exported = "false"/>
        <!-- [END USER MANUAL SERVICE] -->

Я не вижу здесь вызовов stopForeground() ... Я не верю, что stopService() и cancelNotification() будут работать, т.к. stopService() не завершает синхронно службу, а cancelNotification() не может удалить уведомление службы, которая (фактически) все еще на переднем плане.

greeble31 30.10.2018 14:46

Забыл прикрепить сниппет с отменой сервиса с помощью stopForeground. Должно быть так же.

redrom 30.10.2018 15:11
2
2
2 860
2

Ответы 2

Два отдельных вызова stopForeground(int) вызывают такое поведение. stopForeground(int) принимает поразрядную комбинацию флагов и не должен вызываться дважды подряд (потому что первый вызов приведет к тому, что ваша служба перестанет быть службой переднего плана, а это означает, что использовать stopForeground() для связи с ней больше нецелесообразно). Я даже не уверен, каково задокументированное поведение в этом случае.

РЕШЕНИЕ

Просто позвоните stopForeground(true) независимо от версии ОС.

Не вижу смысла запускать сервис из WorkManager.

Другие вопросы по теме