Android - 已取消的通知不断出现

Android - Canceled notifications keeps reappearing

我看到了一些这样的问题,但其中 none 解决了我的问题。 我正在通过 AlarmManager 启动后台服务。每次服务启动时,它都会检查它是否已在 SharedPreferences 中被禁用,如果没有,它会重新安排一个新的自身实例并继续,遵循以下替代路径:

  1. 如果用户想使用GPS,它会等待用户的位置并使用 它调用 REST 端点;
  2. 如果用户不想使用 GPS,它会使用存储在 prefs 中的位置。

HTTP 调用的结果(超时:30 秒)是一个 JSONObject,它生成 0-n 个通知(取决于它找到了多少 "close objects") .

我的问题是:通知,即使被用户取消(滑动或打开),也经常重新出现,就好像它们从未显示过一样。它永远不应该发生,因为 Web 服务会收到一个排除的对象 ID 列表,该列表每次都会更新。

这里是代码:

ScannerService.java

package com.kiulomb.itascanner.service;

import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import com.kiulomb.itascanner.R;
import com.kiulomb.itascanner.network.HTTPRequestManager;
import com.kiulomb.itascanner.network.HTTPResponseListener;
import com.kiulomb.itascanner.network.URLs;
import com.kiulomb.itascanner.pref.FilterPreferencesManager;
import com.kiulomb.itascanner.pref.NotificationsHistoryManager;
import com.kiulomb.itascanner.pref.PrefConstants;
import com.kiulomb.itascanner.utils.Haversine;
import com.kiulomb.itascanner.utils.MyConfiguration;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

public class ScannerService extends Service {
    private static final String TAG = ScannerService.class.getSimpleName();

    private boolean locationFound = false;
    private boolean withoutLocation = false;
    private LocationManager mLocationManager = null;

    private final Timer myTimer = new Timer();
    private final long TIMEOUT = 20000;
    TimerTask myTask = new TimerTask() {
        public void run() {
            try {
                Log.i(TAG, "Timeout is over, trying to stop service (location found? " + locationFound + ")");
                if (!locationFound) {
                    stopSelf();
                }
            } catch (Exception e) {
                Log.e(TAG, "Could not stop service after time: " + e.getMessage());
            }
        }
    };

    private LocationListener[] mLocationListeners = new LocationListener[] {
            new LocationListener(LocationManager.GPS_PROVIDER),
            new LocationListener(LocationManager.NETWORK_PROVIDER)
    };

    private boolean alreadySearching = false;

    private class LocationListener implements android.location.LocationListener {
        Location mLastLocation;

        LocationListener(String provider) {
            Log.i(TAG, "LocationListener is " + provider);
            mLastLocation = new Location(provider);
        }

        @Override
        public void onLocationChanged(final Location location) {
            Log.i(TAG, "onLocationChanged: " + location);

            if (withoutLocation) {
                return;
            }

            ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

            NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
            boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();

            if (location != null) {
                if (isConnected) {
                    mLastLocation.set(location);
                    locationFound = true;

                    Log.i(TAG, "already searching? " + alreadySearching);
                    if (!alreadySearching) {
                        findClosest(location.getLatitude(), location.getLongitude());
                    }
                    alreadySearching = true;
                } else {
                    Log.e(TAG, "no connectivity, ending service");
                    stopSelf();
                }
            } else {
                Log.e(TAG, "no position, ending service");
                stopSelf();
            }
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.i(TAG, "onProviderDisabled: " + provider);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.i(TAG, "onProviderEnabled: " + provider);
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.i(TAG, "onStatusChanged: " + provider);
        }
    }

    private void initializeLocationManager() {
        Log.d(TAG, "initializeLocationManager");
        if (mLocationManager == null) {
            mLocationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
        }
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");
        // super.onStartCommand(intent, flags, startId);
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        Log.d(TAG, "onCreate");

        SharedPreferences pref = getSharedPreferences(PrefConstants.PREF_APP_FILE, MODE_PRIVATE);
        if (pref.getBoolean(PrefConstants.PREF_APP_SERVICE_ENABLED, PrefConstants.PREF_APP_SERVICE_ENABLED_DEFAULT)) {
            Intent intent = new Intent(ScannerService.this, ScannerService.class);
            PendingIntent pintent = PendingIntent.getService(ScannerService.this, 0, intent, 0);
            AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
            Calendar cal = Calendar.getInstance();
            alarm.set(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis() + 60000, pintent); // or setExact() // TODO custom time
            // alarm.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 60000, pintent);

            if (!pref.getBoolean(PrefConstants.PREF_APP_SERVICE_CUSTOMCENTER, PrefConstants.PREF_APP_SERVICE_CUSTOMCENTER_DEFAULT)) {
                // use GPS
                initializeLocationManager();
                try {
                    mLocationManager.requestLocationUpdates(
                            LocationManager.NETWORK_PROVIDER,
                            MyConfiguration.LOCATION_INTERVAL,
                            MyConfiguration.LOCATION_DISTANCE,
                            mLocationListeners[1]);
                } catch (SecurityException ex) {
                    Log.e(TAG, "fail to request location update, ignore", ex);
                } catch (IllegalArgumentException ex) {
                    Log.e(TAG, "network provider does not exist, " + ex.getMessage());
                }

                try {
                    mLocationManager.requestLocationUpdates(
                            LocationManager.GPS_PROVIDER,
                            MyConfiguration.LOCATION_INTERVAL,
                            MyConfiguration.LOCATION_DISTANCE,
                            mLocationListeners[0]);
                } catch (SecurityException ex) {
                    Log.e(TAG, "fail to request location update, ignore", ex);
                } catch (IllegalArgumentException ex) {
                    Log.e(TAG, "gps provider does not exist " + ex.getMessage());
                }
            } else {
                withoutLocation = true;

                // do not use GPS
                String[] savedNotifCenter = pref.getString(PrefConstants.PREF_APP_SERVICE_CENTER, PrefConstants.PREF_APP_SERVICE_CENTER_DEFAULT).split(",");
                double savedLat = Double.parseDouble(savedNotifCenter[0]);
                double savedLng = Double.parseDouble(savedNotifCenter[1]);

                locationFound = true; // prevent the service from stopping
                findClosest(savedLat, savedLng);
            }
        } else {
            stopSelf();
            return;
        }

        /*if (isForeground(getPackageName())) {
            Log.i(getClass().getSimpleName(), "application is in foreground, stopping service");
            stopSelf();
            return;
        }*/

        myTimer.schedule(myTask, TIMEOUT);
    }

    public boolean isForeground(String myPackage) {
        ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> runningTaskInfo = manager.getRunningTasks(1);
        ComponentName componentInfo = runningTaskInfo.get(0).topActivity;
        return componentInfo.getPackageName().equals(myPackage);
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy");
        super.onDestroy();
        if (mLocationManager != null) {
            for (LocationListener mLocationListener : mLocationListeners) {
                try {
                    mLocationManager.removeUpdates(mLocationListener);
                } catch (SecurityException se) {
                    Log.e(TAG, "security exception", se);
                } catch (Exception ex) {
                    Log.e(TAG, "fail to remove location listeners, ignore", ex);
                }
            }
        }
    }

    private void findClosest(final double lat, final double lng) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String url = URLs.buildURL(URLs.NOTIFICATIONS);
                url += "?lat=" + lat;
                url += "&lng=" + lng;

                final SharedPreferences pref = getSharedPreferences(PrefConstants.PREF_APP_FILE, MODE_PRIVATE);
                if (pref.contains(PrefConstants.PREF_APP_SERVICE_RADIUS)) {
                    url += "&radius=" + pref.getInt(PrefConstants.PREF_APP_SERVICE_RADIUS, PrefConstants.PREF_APP_SERVICE_RADIUS_DEFAULT);
                }

                url += "&limit=" + PrefConstants.PREF_APP_MAP_LIMIT_DEFAULT;

                if (pref.contains(PrefConstants.PREF_APP_SERVICE_IV)) {
                    url += "&iv=" + pref.getInt(PrefConstants.PREF_APP_SERVICE_IV, PrefConstants.PREF_APP_SERVICE_IV_DEFAULT);
                }

                String exclusionsNumbers = getExcludedNumbersParam();
                if (exclusionsNumbers.length() > 0) {
                    url += "&exNum=" + exclusionsNumbers;
                }

                final NotificationsHistoryManager notificationsHistoryManager = new NotificationsHistoryManager(ScannerService.this);
                final List<Long> excludedIds = notificationsHistoryManager.getAlreadyFoundObjects();
                String exclusionsIds = getExcludedIdsParam(excludedIds);
                if (exclusionsIds.length() > 0) {
                    url += "&exId=" + exclusionsIds;
                }

                /*final long lastId = pref.getLong(PrefConstants.PREF_SERVICE_LAST_ID, 0L);
                url += "&li=" + lastId;*/

                final Context context = ScannerService.this;
                HTTPRequestManager requestManager = new HTTPRequestManager(context, url, true, null, new HTTPResponseListener() {
                    @Override
                    public void onSuccess(JSONObject response) {
                        try {
                            JSONArray responseArray = response.getJSONArray("objects");
                            final String foundString = getString(R.string.found);
                            final String inCityString = getString(R.string.in_city);
                            final String expiringString = getString(R.string.expiring);
                            final DateFormat sdf = SimpleDateFormat.getTimeInstance();
                            final Resources res = getResources();
                            final String packageName = getPackageName();
                            final String mapsApiKey = getString(R.string.google_maps_key);
                            final boolean notifClickAutoCancel = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_NOTIFCANCEL, PrefConstants.PREF_APP_SERVICE_NOTIFCANCEL_DEFAULT);
                            final boolean notifExpiredAutoCancel = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_NOTIFCANCELEXPIRED, PrefConstants.PREF_APP_SERVICE_NOTIFCANCELEXPIRED_DEFAULT);
                            final boolean mapPicture = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_MAPPICTURE, PrefConstants.PREF_APP_SERVICE_MAPPICTURE_DEFAULT);
                            final Locale defaultLocale = Locale.getDefault();

                            Calendar calendar = Calendar.getInstance();
                            // long maxId = lastId;
                            for (int i = 0; i < responseArray.length(); i++) {
                                try {
                                    final MyEntity p = MyEntity.fromJSONLight(responseArray.getJSONObject(i));
                                    // it should never happen, but notifications are shown many times :/
                                    if (!excludedIds.contains(p.getId())) {
                                        excludedIds.add(p.getId());
                                        // maxId = Math.max(p.getId(), maxId);
                                        final double iv = p.getIV();
                                        final long expirationFixed = (p.getDisappearTime() - System.currentTimeMillis() - 2000);

                                        final Calendar expirationTime = (Calendar) calendar.clone();
                                        // now.add(Calendar.SECOND, (int) ((p.getDisappearTime() - System.currentTimeMillis() / 1000) - 2));
                                        expirationTime.setTimeInMillis(expirationTime.getTimeInMillis() + expirationFixed);

                                        final int distance = (int) Math.round(1000 * Haversine.distance(lat, lng, p.getLatitude(), p.getLongitude()));

                                        String cityName = null;
                                        Geocoder gcd = new Geocoder(context, defaultLocale);
                                        List<Address> addresses = gcd.getFromLocation(p.getLatitude(), p.getLongitude(), 1);
                                        if (addresses.size() > 0) {
                                            cityName = addresses.get(0).getLocality();
                                        }
                                        final String cityNameParam = cityName;
                                        new Thread(new Runnable() {
                                            @Override
                                            public void run() {
                                                sendNotification((int) (p.getId()),
                                                                 foundString + " " + p.getName() + (iv > 0 ? " " + iv + "%" : "") + (cityNameParam != null ? " " + inCityString + " " + cityNameParam : ""),
                                                                 expiringString + " " + sdf.format(expirationTime.getTime()) + " - " + distance + "m" + (movesStringParam != null ? " (" + movesStringParam + ")" : ""),
                                                                 p,
                                                                 res,
                                                                 packageName,
                                                                 notifClickAutoCancel,
                                                                 notifExpiredAutoCancel,
                                                                 expirationFixed,
                                                                 mapsApiKey,
                                                                 mapPicture);
                                            }
                                        }).start();
                                    }
                                } catch (Exception e) {
                                    Log.e(TAG, "error", e);
                                }
                            }

                            notificationsHistoryManager.saveAlreadyFoundObjects(excludedIds);

                            stopSelf();
                        } catch (Exception e) {
                            Log.e(TAG, "error in reading JSONArray", e);
                            stopSelf();
                        }
                    }

                    @Override
                    public void onError(int errorCode) {
                        stopSelf();
                    }
                });

                RequestQueue requestQueue = Volley.newRequestQueue(context);
                requestQueue.add(requestManager);
            }
        }).start();
    }

    private String getExcludedNumbersParam() {
        String exclusionsNumbers = "";
        List<Integer> excludedNumbers = new FilterPreferencesManager(ScannerService.this).getNotificationsExcludedNumbers();
        int sizeNumbers = excludedNumbers.size();
        for (int i = 0; i < sizeNumbers; i++) {
            exclusionsNumbers += excludedNumbers.get(i);

            if (i < sizeNumbers - 1) {
                exclusionsNumbers += ",";
            }
        }

        return exclusionsNumbers;
    }

    private String getExcludedIdsParam(List<Long> excludedIds) {
        String exclusionsIds = "";

        int sizeIds = excludedIds.size();
        for (int i = 0; i < sizeIds; i++) {
            exclusionsIds += excludedIds.get(i);

            if (i < sizeIds - 1) {
                exclusionsIds += ",";
            }
        }
        return exclusionsIds;
    }

    private Locale locale = Locale.getDefault();

    private void sendNotification(final int notificationId,
            final String title,
            final String message,
            final MyEntity entity,
            final Resources res,
            final String packageName,
            final boolean autoClickCancel,
            final boolean autoExpiredCancel,
            final long expirationFromNow,
            final String mapsApiKey,
            final boolean mapPicture) {

        final double entityLat = entity.getLatitude();
        final double entityLng = entity.getLongitude();

        Intent mapIntent = null;
        try {
            String urlAddress = "http://maps.google.com/maps?q=" + entityLat + "," + entityLng;
            mapIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlAddress));
        } catch (Exception e) {
            Log.e(TAG, "error in notification intent preparation", e);
        }
        PendingIntent pendingIntent = PendingIntent.getActivity(ScannerService.this, 0, mapIntent, PendingIntent.FLAG_CANCEL_CURRENT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        int drawable = res.getIdentifier("entity" + String.format(locale, "%04d", entity.getNumber()) + "big", "drawable", packageName);
        final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ScannerService.this)
                .setSmallIcon(drawable)
                .setContentTitle(title)
                .setContentText(message)
                .setAutoCancel(autoClickCancel)
                .setSound(defaultSoundUri)
                .setPriority(Notification.PRIORITY_HIGH)
                .setLights(ContextCompat.getColor(ScannerService.this, R.color.colorPrimary), 500, 2000);

        if (mapPicture) {
            String imageUrl = "https://maps.googleapis.com/maps/api/staticmap"
                    + "?center=" + entityLat + "," + entityLng
                    + "&zoom=14"
                    + "&scale=false"
                    + "&size=450x275"
                    + "&maptype=roadmap"
                    + "&key=" + mapsApiKey
                    + "&format=jpg"
                    + "&visual_refresh=true";

            Log.i(getClass().getSimpleName(), "generated url for notification image: " + imageUrl);
            Bitmap bmURL = getBitmapFromURL(imageUrl);
            if (bmURL != null) {
                notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(bmURL));
            }
        }

        if (mapIntent != null) {
            notificationBuilder.setContentIntent(pendingIntent);
        }

        if (autoExpiredCancel) {
            Log.i(getClass().getSimpleName(), "setting notification timer for expiration, id: " + notificationId + ", expiring in " + expirationFromNow + "ms");

            Timer timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    Log.i(getClass().getSimpleName(), "canceling notification expired, id: " + notificationId);
                    notificationManager.cancel(notificationId);
                }
            }, expirationFromNow);
        }
    }
    // }

    private Bitmap getBitmapFromURL(String strURL) {
        try {
            URL url = new URL(strURL);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.connect();
            InputStream input = connection.getInputStream();
            return BitmapFactory.decodeStream(input);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

通知历史管理器

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class NotificationsHistoryManager {

    private final static String PREF_FILE = "nh";
    private final static String PREF_FOUND_KEY = "f";

    private SharedPreferences pref;

    public NotificationsHistoryManager(Context context) {
        pref = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
    }

    public void saveAlreadyFoundObjects(List<Long> found) {
        Set<String> idsString = new HashSet<>();
        int size = found.size();
        for (int i = Math.max(0, size - 200); i < size; i++) {
            long f = found.get(i);
            idsString.add(f + "");
        }

        pref.edit().putStringSet(PREF_FOUND_KEY, idsString).apply();
    }

    public List<Long> getAlreadyFoundObjects() {
        List<Long> excluded = new ArrayList<>();

        for (String id : pref.getStringSet(PREF_FOUND_KEY, new HashSet<String>())) {
            try {
                excluded.add(Long.parseLong(id));
            } catch (Exception e) {
                Log.e(getClass().getSimpleName(), "error in parsing string '" + id + "' to long id: " + e.getMessage());
            }
        }

        return excluded;
    }

    public void clean() {
        pref.edit().clear().apply();
    }
}

注意:启动 MainActivity 时,它会检查服务实例是否 运行,如果不是,它会使用 AlarmManager 安排一个新实例。我认为这是问题的原因,但如您所见,该服务每次都会检查已通知的内容并跳过它。 我尝试将 START_STICKY 更改为 NOT_STICKY、使用首选项处理重复 ID、同步操作……我不知道还能尝试什么。请帮助我 :) 如果您需要更多详细信息,请询问。

谢谢!

分享我的发现……我明白问题出在哪里了。 看一下 NotificationsHistoryManager:它使用一个 Set 来保存找到的对象列表(SharedPreferences 中唯一可用的 "list" 对象类型),只保存最后找到的 200 个对象(旧的过期,所以它没有意义保留它们)。 问题是:集合不是有序列表。我保存的 200 个对象不是最后添加的,因为当我从 pref (getAlreadyFoundObjects()) 读取它们时,它们是写在一个集合中的,"randomly" 有序。 我不得不改变我存储它们的方式,创建一个自定义字符串(逗号分隔值),以确保它们按照我想要的顺序保存。

希望对大家有所帮助。