Search code examples
androidservicenotificationsgpsalarmmanager

Android - Canceled notifications keeps reappearing


I saw some questions like this, but none of them solved my problem. I'm starting a background service through an AlarmManager. Everytime the service starts, it checks if it has been disabled in SharedPreferences and, if not, it reschedule a new instance of itself and goes on, following these alternative paths:

  1. if the user wants to use GPS, it waits for user's position and uses it to call a REST endpoint;
  2. if the user does not want to use GPS, it uses the position stored in prefs.

The result of the HTTP call (timeout: 30 seconds) is a JSONObject, which generates 0-n notifications (depending on how many "close objects" it finds).

My problem is: notifications, even if canceled by the user (sliding on them or opening them), often reappears, as if they were never shown. It should never happen, because the web service receives a list of excluded object ids that are updated every time.

Here the code:

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;
        }
    }
}

Notifications history manager

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();
    }
}

Note: when MainActivity is started, it checks if an instance of the service is running and, if not, it schedules a new one with an AlarmManager. I thought it was the cause of the problem, but the service, as you see, checks every time what has already been notified and skip it. I tried changing START_STICKY to NOT_STICKY, using preferences to handle duplicate IDs, synchronizing operations... I don't know what else to try. Please, help me :) If you need any more details, just ask.

Thank you!


Solution

  • To share what I found... I understood what the problem is. Take a look at NotificationsHistoryManager: it uses, to save the list of found object, a Set (the only "list" object type available in SharedPreferences), saving only the last 200 objects found (old ones expires, so it is meaningless to keep them). THE PROBLEM IS: SET ARE NOT ORDERED LIST. The 200 objects I save are not the LAST added, because when I read them from pref (getAlreadyFoundObjects()) they are written in a set, "randomly" ordered. I had to change the way I stored them, creating a custom string (comma separated values), to be sure they are saved in the order I want.

    Hope it helps someone.