I am trying to implement activity transition updates in the background of my application. My goal is to request activity transition updates once and get activity transition updates all the time in the background. To achieve this, I implemented the following:
PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 100, intent,
PendingIntent.FLAG_NO_CREATE);
if (pi == null) {
pi = PendingIntent.getBroadcast(mApplicationContext, 100,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
Task<Void> task = ActivityRecognition.getClient(mApplicationContext)
.requestActivityTransitionUpdates(atr, pi);
task.addOnCompleteListener(task1 -> {
if (task1.isSuccessful()) {
Log.v(TAG, "Travel behavior activity-transition-update set up");
} else {
Log.v(TAG, "Travel behavior activity-transition-update failed set up: " +
task1.getException().getMessage());
task1.getException().printStackTrace();
}
});
}
So, in this code if PendingIntent.getBroadcast(mApplicationContext, 100, intent,PendingIntent.FLAG_NO_CREATE); returns null, I call requestActivityTransitionUpdates method. Otherwise, I don't request, because the activity transition updates are already been requested with the pending intent.
However, this code doesn't work. After the first requestActivityTransitionUpdates, PendingIntent.getBroadcast(mApplicationContext, 100, intent, PendingIntent.FLAG_NO_CREATE) returns a pending intent and I don't call requestActivityTransitionUpdates and I stop getting transition updates.
If I use the following code by removing the PendingIntent.getBroadcast(mApplicationContext, 100, intent, PendingIntent.FLAG_NO_CREATE) line:
PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 100,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
Task<Void> task = ActivityRecognition.getClient(mApplicationContext)
.requestActivityTransitionUpdates(atr, pi);
task.addOnCompleteListener(task1 -> {
if (task1.isSuccessful()) {
Log.v(TAG, "Travel behavior activity-transition-update set up");
} else {
Log.v(TAG, "Travel behavior activity-transition-update failed set up: " +
task1.getException().getMessage());
task1.getException().printStackTrace();
}
});
I keep getting the transition updates. But in this way, I have to request transition updates every time when the app opens. And after each transition update request, the API returns the current activity in my BroadcastReceiver class even though there is no actual activity transition happened.
So, is there a way to request transition updates once and keep getting transition updates all the time?
You can use the second way.
But as you say the broadcast receiver will be notified at each app run because the transition API returns the last detected activity.
You can get that event time and ignore it if it's an old one or if it's repeated by saving the last received one in some where such as db or shared preferences.
Related
The constructor to AlarmManager.AlarmClockInfo takes a PendingIntent, described as "an intent that can be used to show or edit details of the alarm clock". Where is this used by the system? I don't see anything in the Android 6.0 UI that would seem to trigger that PendingIntent.
The PendingIntent is returned by getShowIntent() in AlarmManager.AlarmClockInfo:
public PendingIntent getShowIntent() {
return mShowIntent;
}
and it's used in the onClick() method of StatusBarHeaderView:
PendingIntent showIntent = mNextAlarm.getShowIntent();
if (showIntent != null && showIntent.isActivity()) {
mActivityStarter.startActivity(showIntent.getIntent(), true /* dismissShade */);
}
Visually, the thing the user clicks on to invoke the PendingIntent is the date/time of the alarm, shown in the following screenshot in grey to the right of the alarm clock icon:
I'm very inexperienced with Android and having trouble trying to get an AlarmManager to basically run a small activity that spawns a notification every hour (for testing purposes I lowered it to every 10 seconds.
However it doesn't seem like the activity gets called at all. Logcat shows nothing and no Toast appears, let alone notifications being created.
Context may be the issue, I didn't entirely understand what I was meant to be passing there so I might have made a mistake by passing in MainActivity.this but I couldn't figure out what would be more appropriate when I tried to Google it.
This code is contained in my MainActivity's OnCreate method
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
task = enterTask.getText().toString();
if (!task.equals("Enter task here...") &&
!task.equals("")) {
Toast.makeText(getApplicationContext(),
"Saved task " + task,
Toast.LENGTH_LONG).show();
SharedPreferences.Editor editor = prefs.edit();
editor.putString("Current_Task", task);
editor.commit();
Context context = MainActivity.this;
AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent(context, Notify.class);
PendingIntent pi=PendingIntent.getBroadcast(context, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(),
//AlarmManager.INTERVAL_HOUR,
10000,
pi);
}
}
});
And this is my Notify activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_notify);
Toast.makeText(getApplicationContext(),
"Notifying",
Toast.LENGTH_LONG).show();
prefs = PreferenceManager.getDefaultSharedPreferences(this);
try {
task = prefs.getString("Current_Task", "");
} catch (Exception e) {
e.printStackTrace();
}
if (!task.equals("")){
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Current task")
.setContentText("Hey! Have you " + task + "?");
mNotificationManager.notify(1, mBuilder.build());
}
}
Can anyone help me understand my mistake?
for testing purposes I lowered it to every 10 seconds
Note that depending on your Android version, it may not occur that frequently. Starting with Android 5.1, the minimum period is one minute.
However it doesn't seem like the activity gets called at all.
Starting an activity periodically via AlarmManager is akin to shooting your users in the face with a shotgun: yes, it gets their attention, but not a very nice way.
In fact, your code to set up the alarms has it right: you are using getBroadcast() for the PendingIntent. This will trigger a BroadcastReceiver, not an Activity. Using a BroadcastReceiver is a far better solution for raising your Notification, as the BroadcastReceiver itself does not have any UI and therefore will not interrupt the user by taking over the foreground.
I recommend that you set up a manifest-registered BroadcastReceiver and put your code from onCreate() (from the preferences onwards) into onReceive() of the BroadcastReceiver.
If you really want to start an activity periodically, and you don't mind your users threatening to do unfortunate things to you, use getActivity() instead of getBroadcast() when creating your PendingIntent.
I am writing unit tests for my app and i want to test, if the alarm is properly cancelled, but i can't find the right solution to do that. I am familiar with the method of checking if the alarm is active by using the PendingIntent.getBroadcast() with FLAG_NO_CREATE flag.
I managed to successfully use the getBroadcast() function repeatedly in same test by first checking if the alarm is not set at the launch of the app and then later to check if the alarm is set, both times returning the expected boolean values.
public void testTimerButtonStartProcess(){
Intent intent = new Intent (appContext, OnAlarmReceiver.class);
intent.putExtra(Scheduler.PERIOD_TYPE, 1);
intent.putExtra(Scheduler.DELAY_COUNT, 0);
intent.setAction(Scheduler.CUSTOM_INTENT_ALARM_PERIOD_END);
boolean alarmUp = (PendingIntent.getBroadcast(appContext, 0, intent,PendingIntent.FLAG_NO_CREATE) != null);
assertFalse("the alarm manager wasnt running when app is lauched", alarmUp);
solo.clickOnView(timerLayout);
instr.waitForIdleSync();
solo.sleep(5000);
alarmUp = (PendingIntent.getBroadcast(appContext, 0, intent,PendingIntent.FLAG_NO_CREATE) != null);
assertTrue("the alarm is set", alarmUp);
}
But when i try to do that in reverse order (first checking if the alarm is set and then later - if it is no longer set), my test failed, because after the second check (when the alarm should be cancelled) the getBroadcast() returned true (i expected to get false).
public void testTimerButtonLongPress(){
solo.clickOnView(timerLayout);
instr.waitForIdleSync();
Intent intent = new Intent (appContext, OnAlarmReceiver.class);
intent.putExtra(Scheduler.PERIOD_TYPE, 1);
intent.putExtra(Scheduler.DELAY_COUNT, 0);
intent.setAction(Scheduler.CUSTOM_INTENT_ALARM_PERIOD_END);
boolean alarmUp = (PendingIntent.getBroadcast(appContext, 0, intent,PendingIntent.FLAG_NO_CREATE) != null);
assertTrue("the alarm manager is running", alarmUp);
solo.clickLongOnView(timerLayout, 1500);
instr.waitForIdleSync();
solo.sleep(3000);
alarmUp = (PendingIntent.getBroadcast(appContext, 0, intent,PendingIntent.FLAG_NO_CREATE) != null);
assertFalse("the alarm manager was not running anymore", alarmUp);
}
I also tried to use the getBroadcast only once after the app cancelled the alarm, but i still got returned true.
Meanwhile i am assured, that the cancellation of the alarm works as expected, because app stops alarming after being "turned off".
So I am developing a social game, where you have a certain ammount of time to do a task. Since it is social, you can chat with the other player.
Everytime you receive a chat message you will also get a notification and upon clicking on it you are redirected to the chat screen with him. Right now this is working good.
What I don't want is, that the user will receive a chat notification while he actually is playing on the PlayActivity.
It would be also good, if already shown notification could be ignored or suspended to a later time while he is on the PlayActivity.
Is this accomplishable?
A different approach could be to cancel all already shown notifications and put the BroadcastReceiver to sleep and revive all notifications and the BroadcastReceiver after the PlayActivity is done?
Not possible either?
Could the app at least ask before leaving the activity? that way i could warn him and if he still leaves, the game could be valued against him.
What I do right now is just a workaround, I make the PlayActivity full screen and check if the focus has changed e.g. if he is dragging down the status bar / notification area. But this is just a hack, something I would really like to do away with it.
As you see, I am not really sure what the right approach here could be. What would a pro Android software developer do in my case?
Thanks in advance!
Right now I am handling notifications by extending a BroadcastReceiver. Code is appended at the end, if you need other parts of my code, let me know!
public class PushBroadcastReceiver extends BroadcastReceiver
{
#Override
public void onReceive(Context context, Intent intent)
{
try
{
JSONObject json =
new JSONObject(
intent.getExtras()
.getString("KEY"));
notify(context,intent,json);
}
catch (JSONException e)
{
L.debug(App.TAG, "JSONException: " + e.getMessage());
}
}
private void notify(Context ctx, Intent i, JSONObject dataObject) throws JSONException
{
NotificationManager nm = (NotificationManager)
ctx.getSystemService(Context.NOTIFICATION_SERVICE);
boolean createNotification = false;
PendingIntent pi = null;
int gameId = 0;
// chat
if (dataObject.getString("KEY_CHAT").equals("VALUE_CHAT")) {
Intent intent = new Intent(ctx, ChatActivity.class);
intent.putExtra("opponentUsername", dataObject.getString(PARSE_JSON_OPPONENT_USERNAME_KEY));
intent.putExtra("gameId", dataObject.getString(PARSE_JSON_GAME_ID_KEY));
pi = PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
gameId = Integer.parseInt(dataObject.getString(PARSE_JSON_GAME_ID_KEY));
createNotification = true;
// game
} else if (dataObject.getString("KEY_GAME").equals("VALUE_GAME")) {
Intent intent = new Intent(ctx, SS6RunningGameActivity.class);
intent.putExtra("gameId", dataObject.getString(PARSE_JSON_GAME_ID_KEY));
gameId = Integer.parseInt(dataObject.getString(PARSE_JSON_GAME_ID_KEY));
pi = PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
createNotification = true;
}
if (createNotification) {
Uri sound = Uri.parse("android.resource://"
+ ctx.getPackageName() + "/" + R.raw.push_notif);
int icon = R.drawable.icon_notification_android;
String tickerText =
dataObject.getString("TEXT");
Notification mNotification = new NotificationCompat.Builder(ctx)
.setContentTitle(ctx.getResources().getString(R.string.app_name))
.setContentText(tickerText)
.setSmallIcon(icon)
.setContentIntent(pi)
.setSound(sound)
.setDefaults(Notification.DEFAULT_VIBRATE)
.setAutoCancel(true)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(tickerText))
.build();
nm.notify(gameId, mNotification);
}
}
There can be many approaches to this.
One of the simplest would be the following:
Use SharedPreferences. Add a boolean value to indicate that PlayActivity is currently running.
In onCreate(Bundle) of PlayActivity, do the following:
// Initialization
SharedPreferences preferences = getSharedPreferences("MY_PREF_FILE_NAME", 0);
Editor edit = preferences.edit();
// Here, "PLAY_ACTIVITY_IS_RUNNING" is the `key` and `true` is the value
// We are saying that `PlayActivity` is running
edit.putBoolean("PLAY_ACTIVITY_IS_RUNNING", true);
edit.commit();
Now, in onPause() of PlayActivity, set this boolean to false - indicating that we are about to leave PlayActivity:
// Initialization
SharedPreferences preferences = getSharedPreferences("MY_PREF_FILE_NAME", 0);
Editor edit = preferences.edit();
// Here, "PLAY_ACTIVITY_IS_RUNNING" is the `key` and `false` is the value
// We are saying that `PlayActivity` is not running anymore
edit.putBoolean("PLAY_ACTIVITY_IS_RUNNING", false);
edit.commit();
Rest is quite simple.
In PushBroadcastReceiver, open SharedPreferences and check for the value assigned to key PLAY_ACTIVITY_IS_RUNNING. If this value is false, continue posting the notifications. Else, collect them for later.
public class PushBroadcastReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
SharedPreferences preferences = context.getSharedPreferences(
"MY_PREF_FILE_NAME", 0);
// Retrieve the value stored for key "PLAY_ACTIVITY_IS_RUNNING"
// The second argument `false` is the default value
// in case the key does not exist - this is logically sound
boolean playActivityIsRunning = preferences.getBoolean(
"PLAY_ACTIVITY_IS_RUNNING", false)
// PlayActivity is running
if (playActivityIsRunning) {
// store JSON objects somewhere and deal with them later
} else {
try {
JSONObject json = new JSONObject(intent.getExtras().getString("KEY"));
notify(context,intent,json);
} catch (JSONException e) {
L.debug(App.TAG, "JSONException: " + e.getMessage());
}
}
}
private void notify(Context ctx, Intent i, JSONObject dataObject) throws JSONException {
....
....
}
}
You will need to find a way to post the pending notifications. One solution for this is to send a separate broadcast to another BroadcastReceiver in onPause() of PlayActivity. But this could be problematic because onPause() is called even when the user is changing orientation. Perhaps this BroadcastReceiver can start working after a 1 second delay? This would be sufficient time for the activity to be recreated - and the value of PLAY_ACTIVITY_IS_RUNNING reset (since onCreate(Bundle) of PlayActivity will be called again).
So, the flow would be:
onPause is called
set the value to false in SharedPreferences
send the broadcast to deal with pending notifications
place the code of BroadcastReceiver inside a Runnable. Post this Runnable with a 1 second delay using a Handler.
inside the BroadcastReciever => check value of PLAY_ACTIVITY_IS_RUNNING in SharedPreferences <= this will be done after 1 second. If the user only changed screen orientation, the value of PLAY_ACTIVITY_IS_RUNNING would be true. Otherwise, if the user is navigated away from PlayActivity, the value would be false.
I have an android activity where there is an EditText and as user types in, it calls the service on every key typed. I believe this is not efficient because more than required calls are being made. So the solution is to have some sort of pause checking there.
if (PauseOfThreeSeconds) {
// call the service here
}
How can I sense a pause and then only call the service?
Start a handler with post delayed for 3 seconds every time the key stroke is made. When ever you get a key store, cancel the runnable that is already in the queue and start a new runnable like i mentioned above.
You should schedule an alarm to start the service on each button press, but also to cancel any previously scheduled alarms so they don't go off as well:
AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
long alarmWaitTime = 3000;
onButtonClicked(View v){
Intent i = new Intent(AndroidAlarmService.this, MyAlarmService.class);
PendingIntent pi = PendingIntent.getService(AndroidAlarmService.this, 0, i, 0);
// Cancel any previously set alarms
alarmManager.cancel(pi);
// set a new alarm
alarmManager.set(AlarmManager.RTC, System.getTimeInMillis() + alarmWaitTime , pi);
}