Android why are Notifications firing late after replacing JobIntentService with WorkManager? - broadcastreceiver

JobIntentService class is deprecated so I tried to replace the class with WorkManager so I can reset calendar date alarms after a device reboot. When the alarms are triggered a Notification is fired. Unfortunately, Notifications are no longer firing at exact alarm time but with a long delay (1 to 3 minutes after the reboot on a Samsung Galaxy S7 phone running Android 8.0.0).
The RebootReceiver class receives the BOOT_COMPLETED action after the device is rebooted. The RebootReceiver enqueues a OneTimeWorkRequest that should run immediately in the RebootWorker class. The Worker is used to reset all of the pendingIntents for Alarm dates that are saved in a Room database. I originally set up the JobIntentService because there could be hundreds of pending alarms that need to be reset after a device reboot so I wanted the alarm resets to occur in the background. I thought I could use WorkManager to immediately execute after the BOOT_COMPLETED action so that the alarm pendingIntents would be immediately reset. Then I expected the alarms to trigger the Notifications to fire at the exact alarm time.
In my test case using WorkManager, I only set up two calendar due dates so there are only two alarms. I also tried adding ".setExpedited()..." to the OneTimeWorkRequest and that did not change anything. It should not take 1-3 minutes for the Notifications to fire after the alarms are triggered.
Update: I added test for SDK_Int >= 26 to receive ACTION_LOCKED_BOOT_COMPLETED and had same delayed firings for the Notifications.
What am I missing here? Why are Notifications firing so late after the alarms are triggered?
AndroidManifest
<manifest ...
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application...
<receiver
android:name=".RebootReceiver"
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</receiver>
</application>
</manifest>
RebootReceiver
public class RebootReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (action != null) {
if (action.equals(Intent.ACTION_LOCKED_BOOT_COMPLETED) || action.equals(Intent.ACTION_BOOT_COMPLETED) ||
action.equals(QUICKBOOT_POWERON) || action.equals(HTC_QUICKBOOT)) {
resetAlarmsWorkRequest = new OneTimeWorkRequest.Builder(RebootWorker.class)
// this also did not work: ".setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)"
.build();
WorkManager.getInstance(context).enqueue(resetAlarmsWorkRequest);
}
}
}
}
}
RebootWorker
public class RebootWorker extends Worker {
private CardRepository reposit;
private AlarmManager alarmManager;
private Intent brIntent3;
private PendingIntent pendingIntent3;
private Context mContext;
public RebootWorker(
#NonNull Context context,
#NonNull WorkerParameters parameters) {
super(context, parameters);
mContext = context;
}
#SuppressLint("UnspecifiedImmutableFlag")
#NonNull
#Override
public Result doWork() {
try {
reposit = new CardRepository(mContext);
alarmManager = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
Calendar cal3 = Calendar.getInstance();
List<Card> resetNotificationsList = reposit.getNotifications();
// Cycle through the Room database rows to get the Notifications data.
for (Card card: resetNotificationsList) {
if (card == null) {
break;
}
cal3.setTimeInMillis(quickcard.getDuedatentime());
if (cal3.getTimeInMillis() > System.currentTimeMillis()) {
brIntent3 = new Intent(mContext, AlarmReceiver.class);
brIntent3.setAction("DueAlarm");
brIntent3.setData(Uri.parse("http://" + quickcard.getId()));
if (Build.VERSION.SDK_INT >= 23) {
pendingIntent3 = PendingIntent.getBroadcast(getApplicationContext(),0, brIntent3,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
} else {
pendingIntent3 = PendingIntent.getBroadcast(getApplicationContext(),0, brIntent3,
PendingIntent.FLAG_UPDATE_CURRENT);
}
int SDK_INT3 = Build.VERSION.SDK_INT;
if (SDK_INT3 >= Build.VERSION_CODES.M) {
if (alarmManager != null) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
cal3.getTimeInMillis(), pendingIntent3);
// I also tried "alarmManager.setAlarmClock()..." here, and that did not work either.
}
} else if (SDK_INT3 >= Build.VERSION_CODES.KITKAT) {
if (alarmManager != null) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, cal3.getTimeInMillis(), pendingIntent3);
}
} else {
if (alarmManager != null) {
alarmManager.set(AlarmManager.RTC_WAKEUP, cal3.getTimeInMillis(), pendingIntent3);
}
}
}
}
return Result.success();
} catch (Throwable throwable) {
Log.e(TAG,"Error tring to reset alarms after devive reboot", throwable);
return Result.failure();
}
}
}

Since you want exact alarm, AlarmManager is your choice.
I don't think WorkManager should be used in your case, since it offers no exact guarantee WHEN it will be executed, it just gives you a promise that it will sometime in the future. All this to improve battery life.
From documentation:
Immediate: Tasks that must begin immediately and complete soon. May be expedited.
Expedited work:
You can use WorkManager to schedule immediate work for execution in the background. You should use Expedited work for tasks that are important to the user and which complete within a few minutes.
So 1 to 3 minutes is what Expedited work offers to you.

Related

How to create a background service in .NET Maui

I'm new to mobile app development and am learning .NET Maui. The app I'm creating needs to listen for Accelerometer events, and send a notification to a web service if the events meet certain criteria. The bit I'm struggling with is how to have the app run in the background, i.e. with no UI visible, without going to sleep, as I'd want the user to close the UI completely. So I'm thinking the app needs to run as some kind of service, with the option to show a UI when needed - how can this be done?
i know it's beign a while but will post an answer for future users!
First we need to understand that background services depends on which platform we use.(thanks Jason) And i will focus on ANDROID, based on Xamarin Documentation (thanks Eli), adapted to Maui.
Since we are working with ANDROID, on MauiProgram we will add the following:
/// Add dependecy injection to main page
builder.Services.AddSingleton<MainPage>();
#if ANDROID
builder.Services.AddTransient<IServiceTest, DemoServices>();
#endif
And we create our Interface for DI which provides us the methods to start and stop the foreground service
public interface IServiceTest
{
void Start();
void Stop();
}
Then, before platform code we need to add Android Permissions on AndroidManifest.xml:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Android Main Activity
public class MainActivity : MauiAppCompatActivity
{
//set an activity on main application to get the reference on the service
public static MainActivity ActivityCurrent { get; set; }
public MainActivity()
{
ActivityCurrent = this;
}
}
And Finally we create our Android foreground service. Check Comments Below. Also on xamarin docs, they show the different properties for notification Builder.
[Service]
public class DemoServices : Service, IServiceTest //we implement our service (IServiceTest) and use Android Native Service Class
{
public override IBinder OnBind(Intent intent)
{
throw new NotImplementedException();
}
[return: GeneratedEnum]//we catch the actions intents to know the state of the foreground service
public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
{
if (intent.Action == "START_SERVICE")
{
RegisterNotification();//Proceed to notify
}
else if (intent.Action == "STOP_SERVICE")
{
StopForeground(true);//Stop the service
StopSelfResult(startId);
}
return StartCommandResult.NotSticky;
}
//Start and Stop Intents, set the actions for the MainActivity to get the state of the foreground service
//Setting one action to start and one action to stop the foreground service
public void Start()
{
Intent startService = new Intent(MainActivity.ActivityCurrent, typeof(DemoServices));
startService.SetAction("START_SERVICE");
MainActivity.ActivityCurrent.StartService(startService);
}
public void Stop()
{
Intent stopIntent = new Intent(MainActivity.ActivityCurrent, this.Class);
stopIntent.SetAction("STOP_SERVICE");
MainActivity.ActivityCurrent.StartService(stopIntent);
}
private void RegisterNotification()
{
NotificationChannel channel = new NotificationChannel("ServiceChannel", "ServiceDemo", NotificationImportance.Max);
NotificationManager manager = (NotificationManager)MainActivity.ActivityCurrent.GetSystemService(Context.NotificationService);
manager.CreateNotificationChannel(channel);
Notification notification = new Notification.Builder(this, "ServiceChannel")
.SetContentTitle("Service Working")
.SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
.SetOngoing(true)
.Build();
StartForeground(100, notification);
}
}
Now we have our foreground Service working on Android, that show a notification ("Service Working"). Every time it starts. I make a show message foreground service to see it better while testing, in your case it suppose to close the app if that's what you want, but the functioning it's the same.
So having our background service working only left a way to call it so on our main page (as example) i will do the following:
MainPage.xaml
<VerticalStackLayout>
<Label
Text="Welcome to .NET Multi-platform App UI"
FontSize="18"
HorizontalOptions="Center" />
<Button
x:Name="CounterBtn"
Text="start Services"
Clicked="OnServiceStartClicked"
HorizontalOptions="Center" />
<Button Text="Stop Service" Clicked="Button_Clicked"></Button>
</VerticalStackLayout>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
IServiceTest Services;
public MainPage(IServiceTest Services_)
{
InitializeComponent();
ToggleAccelerometer();
Services = Services_;
}
//method to start manually foreground service
private void OnServiceStartClicked(object sender, EventArgs e)
{
Services.Start();
}
//method to stop manually foreground service
private void Button_Clicked(object sender, EventArgs e)
{
Services.Stop();
}
//method to work with accelerometer
public void ToggleAccelerometer()
{
if (Accelerometer.Default.IsSupported)
{
if (!Accelerometer.Default.IsMonitoring)
{
Accelerometer.Default.ReadingChanged += Accelerometer_ReadingChanged;
Accelerometer.Default.Start(SensorSpeed.UI);
}
else
{
Accelerometer.Default.Stop();
Accelerometer.Default.ReadingChanged -= Accelerometer_ReadingChanged;
}
}
}
//on accelerometer property change we call our service and it would send a message
private void Accelerometer_ReadingChanged(object sender, AccelerometerChangedEventArgs e)
{
Services.Start(); //this will never stop until we made some logic here
}
}
It's a long Answer and it would be great to have more official documentation about this! Hope it helps! If anyone can provide more info about IOS, Windows, MacCatalyst would be awesome!

Xamarin Forms Application Crashes without log

I'm developing some application using Xamarin Forms that has a function of route following. When it is used in real conditions on a vehicle it crashes whithout any logs in spite of the fact that I'm using AppCenter, i.e. in App.xaml.cs OnStart I added
protected async override void OnStart()
{
AppCenter.Start("android=__mycode___" +
"uwp={Your UWP App secret here};" +
"ios={Your iOS App secret here}",
typeof(Analytics), typeof(Crashes));
bool hadMemoryWarning = await Crashes.HasReceivedMemoryWarningInLastSessionAsync();
ErrorReport crashReport = await Crashes.GetLastSessionCrashReportAsync();
if (crashReport != null)
{
Analytics.TrackEvent(crashReport.StackTrace);
}
}
In fact I tested my app in AppCenter by using Crashes.GenerateTestCrash() and got a test crash report. Besides I caught some errors using this approach. But now there is at least one reason that makes my app crash without any messages to AppCenter. Are there any clues how to resolve this problem?
Try using an actual asynchronous event handler.
private event EventHandler onStart = delegate { };
protected override void OnStart() {
onStart += handleStart; //subscribe
onStart(this, EventArgs.Empty); //raise event
}
private async void handleStart(object sender,EventArgs args) {
onStart -= handleStart; //unsubscribe
try {
AppCenter.Start("android=__mycode___" +
"uwp={Your UWP App secret here};" +
"ios={Your iOS App secret here}",
typeof(Analytics), typeof(Crashes));
bool hadMemoryWarning = await Crashes.HasReceivedMemoryWarningInLastSessionAsync();
ErrorReport crashReport = await Crashes.GetLastSessionCrashReportAsync();
if (crashReport != null) {
Analytics.TrackEvent(crashReport.StackTrace);
}
} catch (Exception ex) {
//...handle exception or log as needed
}
}
OnStart is a simple void method and not an actual event handler. This mean that when made async it will become a fire and forget function which wont allow you to catch any exceptions that may have occurred within it.
If it is not caught at startup then it probably means that you are doing something similar somewhere within the app using a non event async void that goes uncaught and is crashing the application.
Start by doing a search for any async void in your code and checking to make sure that it is an actual event handler.

How to start a lockscreen activity instantaneously after reboot?

I want to start a lockscreen activity after device reboot side by side with the startup notifications like Messages,Viber notifications,Whatsapp etc.How to do this ??.I have made a broadcast receiver which receives BOOT_COMPLETED action and upon that it starts a service that registers the same receiver again with Intent.ACTION_SCREEN_OFF and Intent.ACTION_SCREEN_ON intent filter and that receiver starts the lockscreen activity.Here is my code:
AndroidManifest.xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" ></uses-permission>
<receiver>
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
and of course I declared the service in the manifest.
Here is my Broadcast Receiver class
public class LockScreenBroadCastReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
Log.d("BroadCastReceiver", "ReceivedIntent");
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
Intent myIntent = new Intent(context, LockScreenActivity.class);
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
myIntent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
if (!LockScreenActivity.isActivityRunning) {
context.startActivity(myIntent);
}else{
Log.d("BroadCasrReceiver","LockScreenActivity is running");
}
}else if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
Log.v("LockScreenBroadReceiver","boot completed");
Intent startServiceIntent = new Intent(context,LockScreenService.class);
context.startService(startServiceIntent);
}
}
}
and the service class :
public class LockScreenService extends Service {
LockScreenBroadCastReceiver broadCastReciever;
public static boolean isRunning;
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
isRunning = true;
registerReceiver();
}
#Override
public void onDestroy() {
super.onDestroy();
isRunning = false;
unregisterReceiver(broadCastReciever);
Log.d("LockScreenReceiver", "ReceiverUnregistered");
sendBroadcast(new Intent("RestartLockScreenService"));
}
private void registerReceiver(){
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
broadCastReciever = new LockScreenBroadCastReceiver();
registerReceiver(broadCastReciever,intentFilter);
Log.d("LockScreenReceiver", "ReceiverRegistered");
}
}
This approach is working.However,It's very slow as the broadcast receiver listens to BOOT_COMPLETED broadcast which waits until the device is fully up and working.So,you may lock and unlock the phone several times before you get the lockscreen working.Any ideas??
You can use the intentFilter instace of BootComplate in manifist like this:
<action android:name="android.intent.action.USER_PRESENT" />
I had the same issue. It was fixed by including
<category android:name="android.intent.category.DEFAULT" />
on the receiver. The is a slight delay of 6 seconds on reboot which I'm trying to narrow down.
I hope this works for you

Prevent Reload Activity / Webview when go Back to app and when press the turn off button

I have a fragment inside of an Activity and then, inside of this fragment I also have a webview.
Problem: After lock my screen and unlock pressing the turn off button, my Activity is recreated or my webview is reloaded (not sure what happens).
The same problem occurs when I switch apps in my device.
I tried to change my configChanges and my launchMode. Here is part of my activity in android manifest file:
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"
>
</activity>
Also I tried to check my savedInstanceState on myCreate:
#Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState == null) {
this.init(); // Initialize my webview
}
...
EDIT:
#Override
protected void onResume() {
super.onResume();
//Check if the user is authenticated onResume
String message = getIntent().getStringExtra(MainActivity.FragmentIdentifier);
if(message == null || message.compareTo(MainActivity.showWebViewFragment) == 0){
changeFragment(new WebViewFragment());
}else if (message.compareTo(MainActivity.showLoginFragment) == 0){
changeFragment(new LoginFragment());
}
}
Any ideas or code samples will be appreciated, thanks!
I figured out that I should handle the Instance and check if already saved or not, otherwise my onResume could always reload my webview. Here my new code:
#Override
protected void onResume() {
super.onResume();
//Check if the user is authenticated onResume
String message = getIntent().getStringExtra(MainActivity.FragmentIdentifier);
if (message == null || message.compareTo(MainActivity.showWebViewFragment) == 0) {
/*
Check if the instance is saved. If yes, is not necessary to create a new instance of the webview,
otherwise will always reload.
*/
if (!getSavedInstance()){
changeFragment(new WebViewFragment());
}
} else if (message.compareTo(MainActivity.showLoginFragment) == 0) {
changeFragment(new LoginFragment());
}
}
I declared a new global variable
private Boolean savedInstance = false;
And here my get and set
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
setSavedInstance(true);
}
public Boolean getSavedInstance() {
return savedInstance;
}
public void setSavedInstance(Boolean instance_saved) {
this.savedInstance = instance_saved;
}

Android proximity sensor giving false results

First post here, but stackoverflow has solved soooo many problems for me. This one though, I can't seem to figure out. I'm creating an android app that de-increments and TextView value by 1 with every proximity detection. For some reason, when the Activity starts or resumes (power button), it logs 2 proximity hits. I've double-checked to ensure that I'm not close enough to the detector when pushing the power button.
Here is the code:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.exercise);
//start proximity
startProximitySensor(sensorListener());
}
private SensorEventListener sensorListener(){
listener = new SensorEventListener(){
#Override
public void onAccuracyChanged(Sensor arg0, int arg1) {
// TODO Auto-generated method stub
}
#Override
public void onSensorChanged(SensorEvent event) {
if(event.sensor.getType() == Sensor.TYPE_PROXIMITY){
String maxRange = String.valueOf(mProximitySensor.getMaximumRange());
if(event.values[0] == Float.parseFloat(maxRange)){
updateTextView();
Log.i(TAG,"Proximity Sensor Reading: "+ String.valueOf(event.values[0]));
}
}
}
};
return listener;
}
private void startProximitySensor(SensorEventListener proximitySensorEventListener){
mSensorManager = (SensorManager)getSystemService(getApplicationContext().SENSOR_SERVICE);
mProximitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if(mProximitySensor == null){
Log.i(TAG,"No proximity sensor found!");
}else{
proximityStarted = true;
Log.i(TAG,"Proximity started");
mSensorManager.registerListener(proximitySensorEventListener,mProximitySensor,SensorManager.SENSOR_DELAY_UI);
}
}
I've managed to get around it by creating a timeStamp in onResume, and comparing that to the SystemTime generated on each sensor change. If they differ for anything less than 1 second, then the TextView won't update. This works, but I'd still like to know if I'm missing something.
Thanks in advance