NotificationManager updating/hiding 来自 SyncAdapter 的通知(在 UI 线程之外)
NotificationManager updating/hiding notification from SyncAdapter (outside UI thread)
根据 Whosebug 上的 this thread,应该可以管理来自外部 main/UI 线程的通知。它实际上是。我在 SyncAdapter 中创建通知以通知用户后台同步已开始并更新上传进度,上传完成后我将在定义的超时后取消通知。我的问题是通知自动取消是不可预测的。有时它会自动取消,有时它会在下次同步前显示。
这是整个适配器:
package com.marianhello.bgloc.sync;
import android.accounts.Account;
import android.app.NotificationManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import com.marianhello.bgloc.Config;
import com.marianhello.bgloc.HttpPostService;
import com.marianhello.bgloc.UploadingCallback;
import com.marianhello.bgloc.data.ConfigurationDAO;
import com.marianhello.bgloc.data.DAOFactory;
import com.marianhello.logging.LoggerManager;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
/**
* Handle the transfer of data between a server and an
* app, using the Android sync adapter framework.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter implements UploadingCallback {
private static final int NOTIFICATION_ID = 1;
ContentResolver contentResolver;
private ConfigurationDAO configDAO;
private NotificationManager notifyManager;
private BatchManager batchManager;
private org.slf4j.Logger log;
/**
* Set up the sync adapter
*/
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Set up the sync adapter. This form of the
* constructor maintains compatibility with Android 3.0
* and later platform versions
*/
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
@Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
Config config = null;
try {
config = configDAO.retrieveConfiguration();
} catch (JSONException e) {
log.error("Error retrieving config: {}", e.getMessage());
}
if (config == null) return;
log.debug("Sync request: {}", config.toString());
if (config.hasUrl() || config.hasSyncUrl()) {
Long batchStartMillis = System.currentTimeMillis();
File file = null;
try {
file = batchManager.createBatch(batchStartMillis);
} catch (IOException e) {
log.error("Failed to create batch: {}", e.getMessage());
}
if (file == null) {
log.info("Nothing to sync");
return;
}
log.info("Syncing batchStartMillis: {}", batchStartMillis);
String url = config.hasSyncUrl() ? config.getSyncUrl() : config.getUrl();
HashMap<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.putAll(config.getHttpHeaders());
httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
if (uploadLocations(file, url, httpHeaders)) {
log.info("Batch sync successful");
batchManager.setBatchCompleted(batchStartMillis);
if (file.delete()) {
log.info("Batch file has been deleted: {}", file.getAbsolutePath());
} else {
log.warn("Batch file has not been deleted: {}", file.getAbsolutePath());
}
} else {
log.warn("Batch sync failed due server error");
syncResult.stats.numIoExceptions++;
}
}
}
private boolean uploadLocations(File file, String url, HashMap httpHeaders) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
notifyManager.notify(NOTIFICATION_ID, builder.build());
try {
int responseCode = HttpPostService.postJSON(url, file, httpHeaders, this);
if (responseCode == HttpURLConnection.HTTP_OK) {
builder.setContentText("Sync completed");
} else {
builder.setContentText("Sync failed due server error");
}
return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
log.warn("Error uploading locations: {}", e.getMessage());
builder.setContentText("Sync failed: " + e.getMessage());
} finally {
builder.setOngoing(false);
builder.setProgress(0, 0, false);
builder.setAutoCancel(true);
notifyManager.notify(NOTIFICATION_ID, builder.build());
Handler h = new Handler(Looper.getMainLooper());
long delayInMilliseconds = 5000;
h.postDelayed(new Runnable() {
public void run() {
notifyManager.cancel(NOTIFICATION_ID);
}
}, delayInMilliseconds);
}
return false;
}
public void uploadListener(int progress) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
builder.setProgress(100, progress, false);
notifyManager.notify(NOTIFICATION_ID, builder.build());
}
}
整个项目都是 OSS,所以 full source code is available. To get bigger picture also HttpPostService.java 可能很有趣。
我认为您的问题如下:您 post 在 UI 线程上取消通知,但同时在后台线程上 post 更新。取消和最后更新之间存在竞争条件 - 有时取消是通知管理器获得的最后一个命令,有时它在取消后收到额外的更新(这使他再次弹出通知)。
为什么首先要在主线程上 post 取消?只需查看 uploadListener(int)
中的状态并决定是要更新通知还是取消通知...
我在 this Whosebug thread 中找到了问题的解决方案。
When I changed NOTIFICATION_ID from 1 to [RANDOM_NUMBER], it magically started working. I assume that 1 is somehow reserved, although there is no note in any documentation...
An of course make sure you use the same NOTIFICATION_ID to cancel:
notificationManager.cancel(NOTIFICATION_ID);
根据 Whosebug 上的 this thread,应该可以管理来自外部 main/UI 线程的通知。它实际上是。我在 SyncAdapter 中创建通知以通知用户后台同步已开始并更新上传进度,上传完成后我将在定义的超时后取消通知。我的问题是通知自动取消是不可预测的。有时它会自动取消,有时它会在下次同步前显示。
这是整个适配器:
package com.marianhello.bgloc.sync;
import android.accounts.Account;
import android.app.NotificationManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import com.marianhello.bgloc.Config;
import com.marianhello.bgloc.HttpPostService;
import com.marianhello.bgloc.UploadingCallback;
import com.marianhello.bgloc.data.ConfigurationDAO;
import com.marianhello.bgloc.data.DAOFactory;
import com.marianhello.logging.LoggerManager;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
/**
* Handle the transfer of data between a server and an
* app, using the Android sync adapter framework.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter implements UploadingCallback {
private static final int NOTIFICATION_ID = 1;
ContentResolver contentResolver;
private ConfigurationDAO configDAO;
private NotificationManager notifyManager;
private BatchManager batchManager;
private org.slf4j.Logger log;
/**
* Set up the sync adapter
*/
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Set up the sync adapter. This form of the
* constructor maintains compatibility with Android 3.0
* and later platform versions
*/
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
@Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
Config config = null;
try {
config = configDAO.retrieveConfiguration();
} catch (JSONException e) {
log.error("Error retrieving config: {}", e.getMessage());
}
if (config == null) return;
log.debug("Sync request: {}", config.toString());
if (config.hasUrl() || config.hasSyncUrl()) {
Long batchStartMillis = System.currentTimeMillis();
File file = null;
try {
file = batchManager.createBatch(batchStartMillis);
} catch (IOException e) {
log.error("Failed to create batch: {}", e.getMessage());
}
if (file == null) {
log.info("Nothing to sync");
return;
}
log.info("Syncing batchStartMillis: {}", batchStartMillis);
String url = config.hasSyncUrl() ? config.getSyncUrl() : config.getUrl();
HashMap<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.putAll(config.getHttpHeaders());
httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
if (uploadLocations(file, url, httpHeaders)) {
log.info("Batch sync successful");
batchManager.setBatchCompleted(batchStartMillis);
if (file.delete()) {
log.info("Batch file has been deleted: {}", file.getAbsolutePath());
} else {
log.warn("Batch file has not been deleted: {}", file.getAbsolutePath());
}
} else {
log.warn("Batch sync failed due server error");
syncResult.stats.numIoExceptions++;
}
}
}
private boolean uploadLocations(File file, String url, HashMap httpHeaders) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
notifyManager.notify(NOTIFICATION_ID, builder.build());
try {
int responseCode = HttpPostService.postJSON(url, file, httpHeaders, this);
if (responseCode == HttpURLConnection.HTTP_OK) {
builder.setContentText("Sync completed");
} else {
builder.setContentText("Sync failed due server error");
}
return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
log.warn("Error uploading locations: {}", e.getMessage());
builder.setContentText("Sync failed: " + e.getMessage());
} finally {
builder.setOngoing(false);
builder.setProgress(0, 0, false);
builder.setAutoCancel(true);
notifyManager.notify(NOTIFICATION_ID, builder.build());
Handler h = new Handler(Looper.getMainLooper());
long delayInMilliseconds = 5000;
h.postDelayed(new Runnable() {
public void run() {
notifyManager.cancel(NOTIFICATION_ID);
}
}, delayInMilliseconds);
}
return false;
}
public void uploadListener(int progress) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
builder.setProgress(100, progress, false);
notifyManager.notify(NOTIFICATION_ID, builder.build());
}
}
整个项目都是 OSS,所以 full source code is available. To get bigger picture also HttpPostService.java 可能很有趣。
我认为您的问题如下:您 post 在 UI 线程上取消通知,但同时在后台线程上 post 更新。取消和最后更新之间存在竞争条件 - 有时取消是通知管理器获得的最后一个命令,有时它在取消后收到额外的更新(这使他再次弹出通知)。
为什么首先要在主线程上 post 取消?只需查看 uploadListener(int)
中的状态并决定是要更新通知还是取消通知...
我在 this Whosebug thread 中找到了问题的解决方案。
When I changed NOTIFICATION_ID from 1 to [RANDOM_NUMBER], it magically started working. I assume that 1 is somehow reserved, although there is no note in any documentation...
An of course make sure you use the same NOTIFICATION_ID to cancel: notificationManager.cancel(NOTIFICATION_ID);