I'm writing an android app that tracks the user's location and shows the distance, time and price of the trip in a notification, all this tracked in a ForegroundService. The service tracks the location, price and the time, and I'm updating the notification every second.
I get this TransactionTooLargeException in production for the very first notification update, it only happens on Samsung devices running Android 8.0+.
This is what's happening:
I call the start method from the service:
public void start(MeasureService service) {
service.startForeground(NOTIFICATION_ID, getNotification(0, 0, 0));
}
I call the update method from the service:
public void update(int price, float meters, long time) {
mNotificationManager.notify(NOTIFICATION_ID, getNotification(price, meters, time));
}
Actually, these calls are called right after one another, the crash is coming from the update method. Can this (calling them one after the other) be a problem?
this is the getNotification:
private Notification getNotification(int price, float meters, long seconds) {
if (mNotificationBuilder == null) {
createNotificationBuilder();
}
return mNotificationBuilder
.setCustomContentView(getSmallView(price))
.setCustomBigContentView(getBigView(price, seconds, meters))
.build();
}
where the getSmallView and getBigView methods are like this:
private RemoteViews getSmallView(int price) {
String priceText = ...;
mSmallView.setTextViewText(R.id.price, priceText);
return mSmallView;
}
private RemoteViews getBigView(int price, long seconds, float meters) {
String priceText = ...;
String timeText = ...;
String distanceText = ...;
mBigView.setTextViewText(R.id.price, priceText);
mBigView.setTextViewText(R.id.time, timeText);
mBigView.setTextViewText(R.id.distance, distanceText);
return mBigView;
}
and this is how I create the notificationBuilder:
private void createNotificationBuilder() {
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
mNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID);
mNotificationBuilder = mNotificationBuilder
.setSmallIcon(R.drawable.icon)
.setOngoing(true)
.setContentIntent(getOpenMeasurePageIntent());
}
and the getOpenMeasurePageIntent:
private PendingIntent getOpenMeasurePageIntent() {
Intent launchMeasureIntent = new Intent(mContext, MainActivity.class);
launchMeasureIntent.setAction(...);
launchMeasureIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(mContext, 0, launchMeasureIntent, 0);
}
This is the crash log I get:
Caused by: java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 560988 bytes
16 at android.app.NotificationManager.notifyAsUser(NotificationManager.java:319)
17 at android.app.NotificationManager.notify(NotificationManager.java:284)
18 at android.app.NotificationManager.notify(NotificationManager.java:268)
19 at com.myapp.service.measure.MyNotification.void update(int,float,long) <- this is called from the timer
I found a lot of similar issues online, but they are usually about passing big chunks of data in the intent, which I believe I'm not doing.
Any idea what I might be doing wrong?
i think problem is in updating Notification every seconds.
Solution
i suggest you should update notification only when data likes(distance, price) changed.
Related
I wrote this program to sync data with the server. Before checking it with the server. I wrote a program to send notifications every 15 minutes.
My phone is oppo A71
Android version 7.1
The following code is not working when I closed the app.
MainActivity.java
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(
MyPeriodicWork.class,15, TimeUnit.MINUTES)
.addTag("send data") .build();
WorkManager.getInstance().enqueue(periodicWorkRequest);
MyPeriodicWork.java
public class MyPeriodicWork extends Worker {
private static final String FILE_NAME = "chata.txt";
private static final String TAG = "MyPeriodicWork";
public MyPeriodicWork(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
#NonNull
#Override
public Result doWork() {
showNotif();
Log.e(TAG,"doWork:work is done");
return Result.success();
}
public void showNotif(){
Intent intent = new Intent(getApplicationContext(),MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(),0,intent,0);
Calendar calendar = Calendar.getInstance();
SimpleDateFormat mdformat = new SimpleDateFormat("HH:mm:ss");
String strDate = "Current Time : " + mdformat.format(calendar.getTime());
NotificationCompat.Builder notificationCompat = new NotificationCompat.Builder(getApplicationContext(),"14")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Event Handler")
.setContentText("Helloo"+strDate)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext());
notificationManagerCompat.notify(4,notificationCompat.build());
}
}
Do I need to add some permissions to manifest file. If yes what are those codes.
this problem on Chinese custom rom they have blacklisted all app except some big app like Facebook,whatsapp etc etc
I check it many times using Logs and prevent the notify() method of manager to be called multiple times if not necessary to avoid some overhead. Now I have first notification with a 0% progress then again I create a new notification with a 0% progress again, unfortunately only one notification is shown even their id is unique but later on when the first notification progress gets updated example from 0% to 25% then that's the only time it will show the desired output, a 2 notification with different progress value. I am using only one Notification, Notification Manager, and in Notification Builder since I do not want to create an overlap animation of notification when it gets updated. Is this expected behavior when in the foreground?
public abstract class BaseTaskService extends Service {
private static final String TAG = "BaseTaskService";
private static final String CHANNEL_ID_DEFAULT = "Upload and Download";
private int queue = 0;
private FirebaseFirestore mDatabase;
private final List<Integer> listOfTaskID = new ArrayList<>();
private final SparseIntArray totalUnitList = new SparseIntArray();
private final SparseIntArray completedUnitList = new SparseIntArray();
private Notification notification;
private NotificationManager notificationManager;
private final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID_DEFAULT);
public void taskStarted(int id, boolean isUpload) {
//Increase the number of task
changeNumberOfTasks(1);
//Check if the task is new or not, if new then start a foreground service using id for it and add it to the list
if (!listOfTaskID.contains(id)){
listOfTaskID.add(id);
startForeground(id, notification);
Log.d(TAG, "Foreground Task Created : ID = " + id);
}
//If called by Upload Service, start the service once as a foreground per post
//If called by Download Service, start the service once as a foreground per file
if (isUpload){
//Set a total unit of files per post since one post could incorporate numerous images or files
totalUnitList.append(id, totalUnitList.get(id, 0) + 1);
Log.d(TAG, "Total Units For " + id + ": (" + totalUnitList.get(id) + ")");
}
}
public void taskCompleted() {
changeNumberOfTasks(-1);
}
private synchronized void changeNumberOfTasks(int delta) {
//Update the queue by adding delta value which could be 1 or -1
//Queue will display the overall upload or download of file from different tasks
queue += delta;
Log.d(TAG, "Overall Number of Remaining Task: " + queue);
//If there are no tasks left in queue, stop the service :)
if (queue <= 0) {
Log.d(TAG, "Stopping...");
//In Upload Service if there is no task in our queue it means that all request was finished
//so we need to reset the list of post's total task and completed task to zero
totalUnitList.clear();
completedUnitList.clear();
//Clear all of the id task
listOfTaskID.clear();
//Stop the foreground and remove all notification
stopForeground(true);
//Stop this service, calling this method will dismiss the very recent notification.
stopSelf();
}
}
#Override
public void onCreate() {
super.onCreate();
mDatabase = FirebaseFirestore.getInstance();
if (!isNotificationChannelEnabled(CHANNEL_ID_DEFAULT))
Toast.makeText(this, "Please turn on the notification in the app settings.", Toast.LENGTH_SHORT).show();
}
/*
We could use this line but unfortunately it will no longer work on Android O and above so we'll use the hashcode below.
This line is suppose to use for separating/detaching the Foreground notification from a Service
so that generating a separated unique id for PendingIntent and Finished notification is no longer needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
stopForeground(STOP_FOREGROUND_DETACH);
else
ServiceCompat.stopForeground(this, STOP_FOREGROUND_DETACH);
*/
//For Android O and above
private void createDefaultChannel() {
// Since Android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//If null then initialize the Notification Manager
if (notificationManager == null)
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID_DEFAULT,
"Upload and Download",
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
}
public boolean isNotificationChannelEnabled(String channelId){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if(channelId != null) {
NotificationManager manager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = manager.getNotificationChannel(channelId);
return channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
}
return false;
} else {
return NotificationManagerCompat.from(this).areNotificationsEnabled();
}
}
/**
* Show notification with a progress bar.
* Updating the progress happens here
* This is for DOWNLOAD SERVICE
*/
void showProgressNotification(String caption, long completedUnits, long totalUnits, int id) {
createDefaultChannel();
//If null then initialize the Notification Manager
if (notificationManager == null)
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//Compute the progress
int percentComplete = 0;
if (totalUnits > 0) {
percentComplete = (int) (100 * completedUnits / totalUnits);
}
//To update and separate the notification progress according to its task
notification = notificationBuilder
.setProgress(100, percentComplete, false)
.setContentInfo(String.valueOf(percentComplete +"%"))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setContentTitle(getString(R.string.app_name))
.setContentText(caption)
.setAutoCancel(false)
.setOngoing(true)
.build();
if (!listOfTaskID.contains(id))
Log.d(TAG, "Download Notification Created: ID = " + id);
else
Log.d(TAG, "Download Notification Updated: ID = " + id);
//Notify the manager that we have a new update with notification
notificationManager.notify(id, notification);
}
/**
* Show notification with a progress bar.
* Updating the progress happens here
* This is for UPLOAD SERVICE
*/
void showProgressNotification(String caption, final String path, final int id, boolean isComplete, String title, String desc) {
createDefaultChannel();
//If null then initialize the Notification Manager
if (notificationManager == null)
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//Increment only if it is a successful task
if (isComplete)
completedUnitList.append(id, completedUnitList.get(id,0) + 1);
//Update and compute the progress
double percentComplete = 0;
if (totalUnitList.get(id, 0) > 0) {
//Perform this line if and only the total task is not equal to zero since dividing a number by zero is Error
percentComplete = (100 / totalUnitList.get(id)) * completedUnitList.get(id, 0);
}
notification = notificationBuilder
.setProgress(100, (int) percentComplete, false)
.setContentInfo(String.valueOf((int) percentComplete +"%"))
.setSmallIcon(R.drawable.ic_file_upload_white_24dp)
.setContentTitle(getString(R.string.app_name))
.setContentText(caption)
.setAutoCancel(false)
.setOngoing(true)
.build();
//This if condition is use to avoid repetitive call of notify() and will be triggered only if new task is created
if (!isComplete && !listOfTaskID.contains(id)){
Log.d(TAG, "Upload Notification Created: ID = " + id);
//Notify the manager that we have a new notification
notificationManager.notify(id, notification);
}
else if (isComplete){
Log.d(TAG, "Upload Notification Updated: ID = " + id);
//Notify the manager that we have a new update with notification
notificationManager.notify(id, notification);
//Check now if the number of completed task is equal to the number of total task if yes then show a finish notification
if (completedUnitList.get(id) == totalUnitList.get(id)){
Map<String, Object> details = new HashMap<>();
details.put(getResources().getString(R.string.Description), desc);
//We will use milliseconds to calculate how long is the post and for query
details.put(getResources().getString(R.string.Time_Posted), String.valueOf(new Date().getTime()));
details.put(getResources().getString(R.string.file), true);
if (title != null){
details.put(getResources().getString(R.string.Title),title);
details.put(getResources().getString(R.string.SU).toLowerCase(), Objects.requireNonNull(FirebaseAuth.getInstance().getCurrentUser()).getUid());
}
else
details.put(getResources().getString(R.string.uid), Objects.requireNonNull(FirebaseAuth.getInstance().getCurrentUser()).getUid());
//Make Intent to MainActivity
final Intent intent = new Intent(BaseTaskService.this, SUMain.class)
.putExtra(UploadService.DATA_COLLECTION, path)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
mDatabase.document(path).set(details).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
showFinishedNotification(getString(R.string.upload_success), intent, true, id, true);
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
showFinishedNotification(getString(R.string.upload_failure), intent, false, id, true);
}
});
}
}
}
/**
* Show notification that the activity finished.
*/
void showFinishedNotification(String caption, Intent intent, boolean isSuccess, int id, boolean isUpload) {
createDefaultChannel();
//Since calling a stopSelf() method will kill the service itself and dismissed the very recent Finished notification which is wrong in our case.
//Create a new id for Finished notification that is not bounded from the id of the progress notification, service, and foreground.
String uri = isUpload ? String.valueOf(intent.getParcelableExtra(UploadService.FILE_URI)) : String.valueOf(intent.getParcelableExtra(DownloadService.DOWNLOAD_URI));
//Use the hashcode of current timestamp mixed with some string to make it unique.
int newID = (uri + System.currentTimeMillis()).hashCode();
//Make PendingIntent for notification with the new generated unique id
PendingIntent pendingIntent = PendingIntent.getActivity(this, newID, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
int icon = isSuccess ? R.drawable.ic_done : R.drawable.ic_error_white_24dp;
notification = notificationBuilder
.setProgress(0, 0, false)
.setContentTitle(getString(R.string.app_name))
.setContentIntent(pendingIntent)
.setContentText(caption)
.setContentInfo(null)
.setAutoCancel(true)
.setSmallIcon(icon)
.setOngoing(false)
.build();
//Remove the first notification that has a incremental id which is the notification with progress
notificationManager.cancel(id);
//Show a new notification after removing the progress notification with the new generated unique id
notificationManager.notify(newID, notification);
Log.d(TAG, "Finished Notification: ID = " + newID);
}}
Update:
After doing a lot more debugging, I figured out the cause of my notification not updating: Calling setProgress(0, 0, false) upon completion of the download. For some reason when this method is called before notify(), the upcoming update doesn't actually go through. Adding a NotificationChannel doesn't do anything.
My current workaround is to call setProgress(100, 100, false) so that the user can tell the download is finished and will subsequently get updated.
Original question:
I've got a custom file downloading utility that creates a notification whenever a file is being downloaded. This notification gets updated as the download progresses.
However, I am getting some odd results on API levels above 19. When the download is finished, the NotificationManager doesn't update the notification to inform the user of this. The notification does get updated whenever the download progresses, strangely enough.
Also, when I have the debugger active, the notification does get updated when the download completes. This leads me to believe some sort of race condition is happening here but I can't really seem to find out where or how.
My FileDownloader class:
public static void startGetRequestDownload(final Context context,
String fileUrl,
#Nullable String fileName,
#Nullable Header[] headers,
#Nullable RequestParams requestParams,
final boolean showProgressNotification){
final int notificationID = 0;
final NotificationManager mNotifyManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if(mNotifyManager == null) {
Log.e(TAG, "Failed to get NotificationManager service");
return;
}
final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context);
String notificationTitle = context.getString(R.string.default_filename);
if(fileName != null && !fileName.isEmpty()){
notificationTitle = fileName;
}
mBuilder.setContentTitle(notificationTitle)
.setContentText(context.getString(R.string.download_in_progress))
.setColor(ContextCompat.getColor(context, R.color.app_color_accent))
.setSmallIcon(R.drawable.ic_elab);
String uuid = "Temp_" + System.currentTimeMillis();
final File tempFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString(), uuid);
// Temporarily disable SSL as workaround.
RestClient.getClient().setSSLSocketFactory(MySSLSocketFactory.getFixedSocketFactory());
RestClient.getClient().get(context, fileUrl, headers, requestParams, new FileAsyncHttpResponseHandler(tempFile) {
#Override
public void onStart() {
super.onStart();
if(showProgressNotification){
mBuilder.setProgress(100, 0, false);
mNotifyManager.notify(notificationID, mBuilder.build());
}
}
#Override
public void onProgress(long bytesWritten, long totalSize) {
super.onProgress(bytesWritten, totalSize);
if(showProgressNotification){
int progress = (int) (bytesWritten / totalSize * 100);
mBuilder.setProgress(100, progress, false);
mNotifyManager.notify(notificationID, mBuilder.build());
}
}
#Override
public void onFailure(int statusCode, Header[] headers, Throwable throwable, File file) {
if(showProgressNotification){
mBuilder.setContentText(context.getString(R.string.download_failed));
mBuilder.setProgress(0,0,false);
mNotifyManager.notify(notificationID, mBuilder.build());
}
Log.w(TAG, "File download failed", throwable);
}
#Override
public void onSuccess(int statusCode, Header[] headers, File file) {
FileMetaData metaData = validateResponse(headers);
if(metaData != null){
final File downloadDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString());
File newFile = new File(downloadDirectory, metaData.fileName);
if(file.renameTo(newFile)){
Uri fileUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider", newFile);
Intent actionViewIntent = new Intent(Intent.ACTION_VIEW);
actionViewIntent.setDataAndType(fileUri, metaData.contentType);
actionViewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if(showProgressNotification){
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, actionViewIntent, PendingIntent.FLAG_ONE_SHOT);
mBuilder.setProgress(0,0,false);
mBuilder.setContentText(context.getString(R.string.download_completed));
mBuilder.setContentIntent(pendingIntent);
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
mNotifyManager.notify(notificationID, mBuilder.build());
}
return;
}
}
//Failover
if(showProgressNotification){
mBuilder.setContentText(context.getString(R.string.download_failed));
mNotifyManager.notify(notificationID, mBuilder.build());
}
}
});
}
I figured out the actual reason as to why my notification wasn't getting updated. The Android documentation mentions the following in regards to notification updates:
Caution: The system applies a rate limit to updating notifications. If you post updates to a notification too frequently, the system may drop some notifications.
I was calling notify() every time the download progressed. Obviously, the download progress method was called very often, which resulted in several notifications getting blocked. However, I only saw the last notification getting dropped: The download completion notification.
Limiting the amount of calls to notify() in onProgress() fixed my issue.
Notifications are not always received by my Android phones, even though Cloud Functions always sends them. As a useful side node: WhatsApp messages are always received (I tested it).
I noticed that notifications are received immediately when the phone is charging or when you open the app.
Also, the UID of the user is not always sent to the database even though a notification has been displayed (you'll understand what I mean by reading the code).
My question is: what can I change to receive notifications and then push to database all the time like it's supposed to be?
My notifications are of the "heads-up" type.
I've been trying to optimize my code for days with no success. I need help.
Code structure:
When message is received, push UID of user to database to see how many people have been notified later.
Send notification to user.
public class MyFirebaseMessagingService extends FirebaseMessagingService {
// Firebase instance variables
private FirebaseAuth mFirebaseAuth;
private FirebaseUser mFirebaseUser;
DatabaseReference myURL = FirebaseDatabase.getInstance().getReferenceFromUrl("https://newproj-54a87.firebaseio.com/notified");
#Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// Check if message contains a data payload.
if (remoteMessage.getData() != null) {
mFirebaseAuth = FirebaseAuth.getInstance();
mFirebaseUser = mFirebaseAuth.getCurrentUser();
if(mFirebaseUser != null)
mUID = mFirebaseUser.getUid();
myURL.push().setValue(mUID);
sendNotification(remoteMessage.getData().get("body"), remoteMessage.getData().get("title"), remoteMessage.getData().get("icon"));
}
}
private void sendNotification(String messageBody, String messageTitle, String iconURL) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Bitmap bitmap = getBitmapFromURL(iconURL);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
.setDefaults(DEFAULT_SOUND | DEFAULT_VIBRATE)
.setPriority(Notification.PRIORITY_MAX)
.setStyle(new NotificationCompat.BigTextStyle())
.setSmallIcon(R.drawable.myIcon)
.setLargeIcon(bitmap)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
}
public Bitmap getBitmapFromURL(String strURL) { //from https://stackoverflow.com/a/16007659
try {
URL url = new URL(strURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
Bitmap myBitmap = BitmapFactory.decodeStream(input);
return myBitmap;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
My Cloud Function:
const payLoad = {
data:{
title: name,
body: body,
icon: photoURL
}
};
var options = {
priority: "high",
timeToLive: 1000
};
return admin.messaging().sendToTopic("notify", payLoad, options);
Please tell me if I forgot a piece of information. I'm running on a limited time schedule!
If you are using Android 6.0 or versions above there is a feature called Doze that optimizes battery usage. When your phone is Idle (and it is not charging) it only receives high priority FCM notifications, and probably that's your case.
Android documentation says:
FCM is optimized to work with Doze and App Standby idle modes by means
of high-priority FCM messages. FCM high-priority messages let you
reliably wake your app to access the network, even if the user’s
device is in Doze or the app is in App Standby mode. In Doze or App
Standby mode, the system delivers the message and gives the app
temporary access to network services and partial wakelocks, then
returns the device or app to the idle state.
So if are using FCM data messages your backend should start sending the field "priority" : "high".
Ref: https://firebase.google.com/docs/cloud-messaging/concept-options
If I have these two geofences, after registering these geofences I should get notified when I'm entering or exiting the circumference of these circles. However, I don't want my App to send a notification if I'm moving through the common area, i.e. from once circle to another.
Is it possible? If so, then how?
You will have to use a class for monitoring your fencing:
public class GeofenceReceiver extends BroadcastReceiver {
Context context;
Intent broadcastIntent = new Intent();
#Override
public void onReceive(Context context, Intent intent) {
this.context = context;
broadcastIntent.addCategory(GeofenceUtils.CATEGORY_LOCATION_SERVICES);
if (LocationClient.hasError(intent)) {
handleError(intent);
} else {
handleEnterExit(intent);
}
}
private void handleError(Intent intent){
// Get the error code
int errorCode = LocationClient.getErrorCode(intent);
// Get the error message
String errorMessage = LocationServiceErrorMessages.getErrorString(
context, errorCode);
// Log the error
Log.e(GeofenceUtils.APPTAG,
context.getString(R.string.geofence_transition_error_detail,
errorMessage));
// Set the action and error message for the broadcast intent
broadcastIntent
.setAction(GeofenceUtils.ACTION_GEOFENCE_ERROR)
.putExtra(GeofenceUtils.EXTRA_GEOFENCE_STATUS, errorMessage);
// Broadcast the error *locally* to other components in this app
LocalBroadcastManager.getInstance(context).sendBroadcast(
broadcastIntent);
}
private void handleEnterExit(Intent intent) {
// Get the type of transition (entry or exit)
int transition = LocationClient.getGeofenceTransition(intent);
// Test that a valid transition was reported
if ((transition == Geofence.GEOFENCE_TRANSITION_ENTER)
|| (transition == Geofence.GEOFENCE_TRANSITION_EXIT)) {
// Post a notification
List<Geofence> geofences = LocationClient
.getTriggeringGeofences(intent);
String[] geofenceIds = new String[geofences.size()];
String ids = TextUtils.join(GeofenceUtils.GEOFENCE_ID_DELIMITER,
geofenceIds);
String transitionType = GeofenceUtils
.getTransitionString(transition);
for (int index = 0; index < geofences.size(); index++) {
Geofence geofence = geofences.get(index);
...do something with the geofence entry or exit. I'm saving them to a local sqlite db
}
// Create an Intent to broadcast to the app
broadcastIntent
.setAction(GeofenceUtils.ACTION_GEOFENCE_TRANSITION)
.addCategory(GeofenceUtils.CATEGORY_LOCATION_SERVICES)
.putExtra(GeofenceUtils.EXTRA_GEOFENCE_ID, geofenceIds)
.putExtra(GeofenceUtils.EXTRA_GEOFENCE_TRANSITION_TYPE,
transitionType);
LocalBroadcastManager.getInstance(MyApplication.getContext())
.sendBroadcast(broadcastIntent);
// Log the transition type and a message
Log.d(GeofenceUtils.APPTAG, transitionType + ": " + ids);
Log.d(GeofenceUtils.APPTAG,
context.getString(R.string.geofence_transition_notification_text));
// In debug mode, log the result
Log.d(GeofenceUtils.APPTAG, "transition");
// An invalid transition was reported
} else {
// Always log as an error
Log.e(GeofenceUtils.APPTAG,
context.getString(R.string.geofence_transition_invalid_type,
transition));
}
}
//Posts a notification in the notification bar when a transition
private void sendNotification(String transitionType, String locationName) {
// Create an explicit content Intent that starts the main Activity
Intent notificationIntent = new Intent(context, MainActivity.class);
// Construct a task stack
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
// Adds the main Activity to the task stack as the parent
stackBuilder.addParentStack(MainActivity.class);
// Push the content Intent onto the stack
stackBuilder.addNextIntent(notificationIntent);
// Get a PendingIntent containing the entire back stack
PendingIntent notificationPendingIntent = stackBuilder
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
// Get a notification builder that's compatible with platform versions
// >= 4
NotificationCompat.Builder builder = new NotificationCompat.Builder(
context);
// Set the notification contents
builder.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(transitionType + ": " + locationName)
.setContentText(
context.getString(R.string.geofence_transition_notification_text))
.setContentIntent(notificationPendingIntent);
// Get an instance of the Notification manager
NotificationManager mNotificationManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
// Issue the notification
mNotificationManager.notify(0, builder.build());
}
You should create listeners for each one of the areas you wish to monitor, lets say listener1 and listener2. To optimize both areas and integrate it, the best approach is to make a grid using MongoDB, which in such case even allows you to integrate more than two points, as you build a grid.
Assuming that you're going to get out a polygon in the form of some Lat-Lon points, then you can generate a grid like the following:
# Method to get the min and max values for the polygon
def get_bounding_box(coords)
# get max and min coords
max = coords.inject({lat:0, lon:0}) do |max, c|
max[:lon] = c[0] if c[0] > max[:lon]
max[:lat] = c[1] if c[1] > max[:lat]
max
end
min = coords.inject({lat:MAX_LAT, lon:MAX_LON}) do |min, c|
min[:lon] = c[0] if c[0] < min[:lon]
min[:lat] = c[1] if c[1] < min[:lat]
min
end
# add a little padding to the max and min
max.each {|k, v| max[k] += 1 }
min.each {|k, v| min[k] -= 1 }
{min: min, max: max}
end
def generate_grid(bounds)
lon_range = bounds[:min][:lon]...bounds[:max][:lon]
lat_range = bounds[:min][:lat]...bounds[:max][:lat]
grid = []
lon_range.each do |lon|
lat_range.each do |lat|
grid << [lon + 0.25, lat + 0.25]
grid << [lon + 0.25, lat + 0.75]
grid << [lon + 0.75, lat + 0.25]
grid << [lon + 0.75, lat + 0.75]
end
end
grid
end
Such approach allows you to achieve very efficient geofencing with smart grids for monitoring target areas:
Most recently MongoDB also added support for Android, thus providing an easy way for your Android App back-end integration. In fact geofencing development with smart distributed data is expected to have a growing number of applications.
This may be an alternative:
All geofences have an id; I am not sure they need to be unique, but for this discussion let's say they must be unique. In your two geofence example let's use ids "fenceHome-1" and "fenceHome-2" for the ones shown and a third one called "someOther-1" which is not shown.
Now what you can do is create a variable to store the current geofence the user is in. In this example it will be a String with the geofence id. Let's call it
String currentGeofence = new String();
When the user enters a new geofence you can now check if the geofenceIds are the same.
/**
geofenceEntered get from the Intent. Should be "fenceHome-1" or "fenceHome-2" or "someOther=1"
*/
public void enteredGeoFence(String geofenceEntered) {
// strip off the "-1" or "-2"
geofenceEntered = geofenceEntered.stripOff();
if (currentGoofence.equals(geofenceEntered) == false} {
// user entered a new geofence Ex: "SomeOther-1" to "fenceHome-1"
sendNotification(geofenceEntered, .....);
currentGeofence = geofencecEntered;
} else {
// user entered a geofence with in the same 'area'. Ex: "fenceHome-1" to "fenceHome-2"
// do nothing
}
}
That is how I would do it. Fiddling with all that math is way too hard. Just set a clever naming convention for you geofence ids. The key is the naming of the geofences.
In the real world currentGeofence would need to be a Collection as a user may be in multiple geofences and the geofenceExit() would have to remove from currentGeofence.
One other thing to remember of the Android Notification manager is: if you send the same notification twice it will only send one notification. This could be used to your advantage.
very schematically:
boolean isTransition1, isTransition2, isTransition, insideCircle1, insideCircle2, insideUnion, insideUnionPrev;
if (isTransition1 | isTransition2) {
insideCircle1 = (dist(currPosition, centerCircle1) < radius1);
insideCircle2 = (dist(currPosition, centerCircle2) < radius2);
insideUnionPrev = insideUnion;
insideUnion = insideCircle1 | insideCircle;
isTransition = (insideUnion != insideUnionPrev);
if (isTransition & insideUnion) println("Moved into region");
if (isTransition & !insideUnion) println("Moved out of region");
}