来自SyncAdapter的NotificationManager更新/隐藏通知(UI外部线程)

时间:2016-08-10 17:51:26

标签: android android-notifications android-syncadapter

根据stackoverflow上的this thread,应该可以管理来自外部主/ 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可用。为了获得更大的图片,HttpPostService.java也可能很有趣。

2 个答案:

答案 0 :(得分:2)

我在this stackoverflow thread找到了解决问题的方法。

  

当我将NOTIFICATION_ID从1更改为[RANDOM_NUMBER]时,它神奇地开始工作。我假设1在某种程度上是保留的,尽管在任何文档中都没有注释......

     

当然要确保使用相同的NOTIFICATION_ID取消:   notificationManager.cancel(NOTIFICATION_ID);

答案 1 :(得分:1)

我认为您的问题如下:您在UI线程上发布通知取消,但同时在后台线程上发布更新。取消和上次更新之间存在竞争条件 - 有时取消是通知管理器获取的最后一个命令,有时它会在取消后接收其他更新(这会使他再次弹出通知)。

为什么你首先在主线程上发布取消?只需检查uploadListener(int)中的状态,然后决定是要更新通知还是取消通知......