Мое приложение использует новый дизайн сервисов 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. Должно быть так же.
Два отдельных вызова stopForeground(int)
вызывают такое поведение. stopForeground(int)
принимает поразрядную комбинацию флагов и не должен вызываться дважды подряд (потому что первый вызов приведет к тому, что ваша служба перестанет быть службой переднего плана, а это означает, что использовать stopForeground()
для связи с ней больше нецелесообразно). Я даже не уверен, каково задокументированное поведение в этом случае.
РЕШЕНИЕ
Просто позвоните stopForeground(true)
независимо от версии ОС.
Не вижу смысла запускать сервис из WorkManager.
Я не вижу здесь вызовов
stopForeground()
... Я не верю, чтоstopService()
иcancelNotification()
будут работать, т.к.stopService()
не завершает синхронно службу, аcancelNotification()
не может удалить уведомление службы, которая (фактически) все еще на переднем плане.