根据stackoverflow上的这个线程,应该可以管理来自主/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,所以完整的源代码是可用的。
我认为你的问题是:你在UI线程上发布通知取消,但同时你在后台线程上发布更新。取消和最后一次更新之间存在竞争条件——有时取消是通知管理器得到的最后一个命令,有时它在取消后收到额外的更新(这使他再次弹出通知)。
为什么首先要在主线程上发布取消呢?只需检查uploadListener(int)
中的状态,并决定您是要更新通知还是取消通知…
我在这个stackoverflow线程中找到了解决我的问题的方法。
当我将NOTIFICATION_ID从1更改为[RANDOM_NUMBER]时,它神奇地开始工作。我假设1在某种程度上是保留的,尽管在任何文档中都没有注释…
当然,确保你使用相同的NOTIFICATION_ID来取消:notificationManager.cancel (NOTIFICATION_ID);