Xamarin.Forms DependencyService From Android (boot-start) Service

NigelWebberNigelWebber USMember ✭✭✭
edited May 14 in Xamarin.Android

Hi

My application is a boot-start Android service. Up until now, I have been starting the service from MainActivity (which calls Forms.Init), this works fine. When the service is started by the system via a receiver on the android.intent.action.BOOT_COMPLETED intent, it obviously cannot call Forms.Init as there is no activity present until such a point as the user taps the notification and launches MainActivity.

I have had a search of the forums and there appears to be no way around this other than a massive re-factoring effort to move platform independent business logic code into Xamarin.Android (and then to duplicate it for Xamarin.iOS). Before I undertake this massive re-factoring and code-duplication effort, is anyone able to confirm that this is, indeed the case?

Background:

My application's service connects to a device over BLE and maintains this connection. It notifies the user when events arrive at the service. All my code dealing with queue's, queue draining, the intermittent nature of a BLE connection etc was in a .net2.0std PCL, but since I cannot use the DependencyService from a service, it looks like I simple CANNOT CALL ANY SHARED CODE from a boot start service, as going so REQUIRES the DependencyService and the DependencyService can only be started from an activity??

Nigel

Posts

  • NigelWebberNigelWebber USMember ✭✭✭
    edited May 16

    Solved this thus, and tested on Android 6,7 and 9 (don't have an 8.0 target currently)

    Short Version

    From a new boot-started BootService, launch an activity which calls Forms.Init before launching your main-service (different to the BootService). BootService then self-stops, MainService inits, and can utilise DependenceService. In Normal-start (user taps icon) in MainActivity detect service running flag, if not running, start it, and schedule an intent to start MainActivity, if service already running, launch FormsApp. Note that I am SingleTop, NoHistory, your milage may differ!

    Long Version

    1. Implement a brand new BootService
    2. In OnStartCommand, have this create and start an intent for an activity (I just used MainActivity)
    3. Call StartForeground(..)
    4. Call StopSelf to terminate this service which is used ONLY to start the activity on a reboot)
    5. Either decorate the BootService, or add it to the manifest.
    6. Make a BootReciever, in OnRecieve either StartForegroundService (>='O') or StartService (<'O')
    7. Back in the manifest or as a decoration, configure your receiver
    8. Setup your Main Service (the one that does all your work. Do your instantiation in OnCreate and call StartForeground in OnStartCommand (NB: >= 'O' --> you have just 5 seconds to call StartForeground from instantiation), make your init prompt!
    9. In your Activity (I just used my MainActivity), OnCreate, Call Forms.Init (and call inits for NuGets/etc)
    10. Within a conditional, test for '!ServiceStarted' flag (your main service, not the BootService)
    11. Call StartForegroundService (>= 'O') or StartService (< 'O')
    12. Call base.OnCreate() followed by Finish to terminate the activity
    13. Before quitting, launch an intent (where there was no supplied intent) to start your activity (this ensures that on bootup, no activity is shown to user, while on initial manual start from icon, intent is shown to user.
    14. return
    15. in the 'else' (eg service already started), do normal launch stuff eg request permissions, and call LoadApplication(...)

    Usage:

    • First Launch from Icon will start main service, and launch MainActivity (BootService will get installed)
    • Reboot phone, system will start BootService via receiver, BootService will start MainActivity OnCreate which will init Xamarin.Forms (thus giving access to DependencyService) and work out that MainService is not started. It will start MainService before terminating the activity.
    • MainService will be able to execute shared code via DependencyService
    • Tapping notification on service or launching App via icon will launch activity as expected

    Works reliably on android 6,7,9 not been able to test on 8.0 as yet. Might well break in a future version, google seems to have gone to war on backgrounding.

    See attached zip for code scratch.veletron.com/DependencyServiceFromBootStartServiceAndroidWorkAround.zip

    Nigel

  • ByBerkByBerk Member ✭✭

    Hi @NigelWebber ,

    I'm trying to do the same service. I'm using dependecy service foreground service notificaiton? Can you help me?

    Thanks

  • NigelWebberNigelWebber USMember ✭✭✭
    edited June 25

    Hi

    All the necessary code is in the zip attached to my post above. I believe you should be able to fathom it out from this, and the text above. Pretty naff oversight on the part of Xamarin to tie the dependency service to forms.init, and insist that it is called from an Activity rather than an application.

    Since writing the above, I ditched most of my interfaces in favour of multi-platform shared code (via a .net 2.0 class lib), thus reducing the need for the dependency service. This shared code exists as a series of Singletons, for instance my BleComms class. Depending on your use-case and how different your code called from the Service is for Android (vs the iOS calls in AppDelegate) is, this might prove to be a better solution?

    My BleComms class now lives in shared code, accessed as a Singleton rather than via an interface:

    public sealed class BleComms
    {
        static BleComms()
        {
            //Dummy ctor For singleton instantiation
        }
    
        public static BleComms SingletonBleComms { get; } = new BleComms();
    
            /// Dummy, used to force pre-init of this class as a singleton
            public bool Init { get; set; }
    
            /// <summary>
            /// Main constructor (private)
            /// </summary>
        private  BleComms()
        {
    
            //normal constructor stuff
    
        }
    }
    

    The above will self-instantiate when BleComms.SingletonBleComms is first used, but to avoid the pregnant pause @ init, I force instantiation before I need it in my Service.OnCreate and AppDelegate.FinishedLaunching calling:

    BleComms.SingletonBleComms.Init = true;

    You can then useBleComms.SingletonBleComms.AnyMethod() from anywhere in your application without worrying about the DependencyService. Beware thread-safe code if you are hitting the singleton from other threads. In my case, my only interaction with it from outside the class is via a couple of ConcurrentQueues for Rx and Tx, and some events that the external code can subscribe to (data arrived, BLE Off, On etc).

    Nigel

  • ByBerkByBerk Member ✭✭

    Hi,

    Thanks for reply.

    I think I worked most of code. The application comes to the screen and closes after 1 second when boot completed. Is this normally? Is there any way to prevent this?

  • NigelWebberNigelWebber USMember ✭✭✭
    edited June 25

    Sounds like its force-closing or dropping right out the bottom of your service - I assume you are running with the debugger connected, and testing service startup works fine when you are starting it from an icon-tap before you start trying to debug boot-start stuff?

    I don't try and start Any visible activity at boot, I just display a notification.

    Are you calling the right method to start your service in the boot receiver?

    public override void OnReceive(Context context, Intent intent)
            {
                if (intent.Action == Intent.ActionBootCompleted)
                {
                    Intent startIntent = new Intent(context, typeof(BootService));
    
                    if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
                        Application.Context.StartForegroundService(startIntent);
                    else
                        Application.Context.StartService(startIntent);
                }
            }
    

    Also note that you must call StartForeground(); within 5 seconds after your service starts.

    Also, check your permissions in the manifest - have you requested the required permissions?

    Start the device log and filter for your app and locate the exception that caused it to exit.

  • ByBerkByBerk Member ✭✭
    edited June 25

    Hi @NigelWebber

    I'm shared all my code and my purpose. I'm already using foreground services notification with dependency service. The foreground notification working on app running. I want start foreground service notification on boot completed.
    Please can you edit my code for boot completed? Not working right now. only work toast message. something is bug again :(

    Thanks in advance

    manifest (edit)

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE" />
    
          <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    
         <receiver android:name=".BootUpReceiver" android:enabled="true"
                   android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >
              <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.DEFAULT" />
              </intent-filter>
            </receiver>
    

    Bootupservice.cs
    ` [BroadcastReceiver]
    [IntentFilter(new[] { Android.Content.Intent.ActionBootCompleted })]
    public class BootUpReceiver : BroadcastReceiver
    {

        public override void OnReceive(Context context, Intent intent)
        {
            Toast.MakeText(context, "Action Boot Completed!", ToastLength.Long).Show();
    
    
            if (intent.Action == Intent.ActionBootCompleted)
            {
                Intent startIntent = new Intent(context, typeof(MainActivity));
    
                if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
                    Android.App.Application.Context.StartForegroundService(startIntent);
                else
                    Android.App.Application.Context.StartService(startIntent);
    
    
    
            }
    

    }
    `
    Depedentservice

    `[assembly: Xamarin.Forms.Dependency(typeof(DependentService))]

    namespace Sample.Droid
    {
    [Service]
    public class DependentService : Service, IService
    {
    public static string messageBody = "test";
    public static string title = "test";

        public static string work = "Yes";
        public void Start()
        {
            var mainintent = new Intent(Android.App.Application.Context,
     typeof(DependentService));
    
    
            MessagingCenter.Subscribe<Bar, string>(this, "OK", (sender, arg) => {
                // do something whenever the "Hi" message is sent
                // using the 'arg' parameter which is a string
                work = arg;
            });
    
    
            MessagingCenter.Subscribe<Bar, Model_Sample>(this, "Hi", (sender, arg) => {
                // do something whenever the "Hi" message is sent
                // using the 'arg' parameter which is a string
                messageBody = arg.Kalan;
                title = arg.Ad;
            });
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
            {
                Android.App.Application.Context.StartForegroundService(mainintent);
    
            }
            else
            {
                Android.App.Application.Context.StartService(mainintent);
            }
        }
        public static bool ServiceStarted { get; private set; }
    
        public override IBinder OnBind(Intent intent)
        {
    
            return null;
        }
        public const int SERVICE_RUNNING_NOTIFICATION_ID = 10000;
    
        [Obsolete]
    
    
        public override StartCommandResult OnStartCommand(Intent intent,
    
        StartCommandFlags flags, int startId)
        {
            // From shared code or in your PCL
    
            CreateNotificationChannel();
            ServiceStarted = false;
    
    
            Device.BeginInvokeOnMainThread(async () =>
            {
                title = await SecureStorage.GetAsync("one");
                messageBody = await SecureStorage.GetAsync("second");
            });
    
    
    
    
    
    
            var notificationIntent = new Intent(this, typeof(SplashActivity));
            notificationIntent.SetFlags(ActivityFlags.SingleTop);
            PendingIntent pendingIntent1 = PendingIntent.GetActivity(this, 0, notificationIntent, PendingIntentFlags.UpdateCurrent);
    
    
            if (Build.VERSION.SdkInt < BuildVersionCodes.O)
            {
                // Notification channels are new in API 26 (and not a part of the
                // support library). There is no need to create a notification
                // channel on older versions of Android.
                var notification = new Notification.Builder(this)
                    .SetContentTitle(title)
    
            .SetContentText(messageBody)
            .SetSmallIcon(Resource.Drawable.logo)
            .SetContentIntent(pendingIntent1)
            .SetVibrate(new long[] { 0L })
            .SetSound(null, null)
            .SetOngoing(true)
    
            .Build();
                notification.Flags = NotificationFlags.NoClear;
    
    
                if (work == "Yes")
                {
                    StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
                }
                else
                {
                    StopForeground(true);
    
                }
                //do you work
                return StartCommandResult.Sticky;
            }
            else
            {
                var notification = new Notification.Builder(this, "10111")
                    .SetContentTitle(title)
        .SetContentText(messageBody)
        .SetSmallIcon(Resource.Drawable.logo)
        .SetContentIntent(pendingIntent1)
        .SetVibrate(new long[] { 0L })
        .SetSound(null, null)
        .SetOngoing(true)
    
        .Build();
    
                notification.Flags = NotificationFlags.NoClear;
                if (work == "Yes")
                {
                    StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
    
                }
                else
                {
                    StopForeground(true);
    
                }                //do you work
                return StartCommandResult.Sticky;
    
            }
    
    
        }
    
        public void CreateNotificationChannel()
        {
            if (Build.VERSION.SdkInt < BuildVersionCodes.O)
            {
                // Notification channels are new in API 26 (and not a part of the
                // support library). There is no need to create a notification
                // channel on older versions of Android.
                return;
            }
            var channelName = "Sample";
            var channelDescription = "Sample Fixed";
            var channel = new NotificationChannel("10111", channelName, NotificationImportance.Low )
            {
                Description = channelDescription,
    
    
            };
            channel.EnableVibration(false);
            channel.SetShowBadge(false);
            channel.Importance = Android.App.NotificationImportance.Low;
            var notificationManager = (NotificationManager)GetSystemService(NotificationService);
            notificationManager.CreateNotificationChannel(channel);
        }
    

    `

    add mainactivity

    `protected override void OnCreate(Bundle savedInstanceState)

    {

            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;
            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    
            if (!DependentService.ServiceStarted)
            {
                //Any custom intents are ignored when the service is not already running
                //instead we start the service before queueing our own intent that will launch 
                //the GUI on the PatientEventsList page.
                //DependencyService.Get<IService>().Start();
    
                var proteusService = new Intent(this, typeof(DependentService));
                if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
                    StartForegroundService(proteusService);
                else
                    StartService(proteusService);
    
                base.OnCreate(savedInstanceState);
    
                Finish();
    
    
    
    
                return;
            }
    
            base.OnCreate(savedInstanceState);
    
    
    
    
    
    
            LoadApplication(new App());
    

    }

    `

  • NigelWebberNigelWebber USMember ✭✭✭

    You appear to be trying to start your MainActivity from your receiver. directly, rather than launching a BootService, and having that start your MainActivity.

    In My application there are two services BootService and MainService. BootService is started by the receiver, this launches MainActivity before calling 'stopself'. In my MainActivity I call Forms.Init, and check to see if my MainService is running, if not, then I start it before calling Finish on MainActivity, if MainService is already running then I launch my UI.

    If you don't need the service to continue running after starting your Activity then you would not need my 'MainService'. In my use-case, normal operation is MainService only to be running. The UI is rarely displayed to the end user.

  • ByBerkByBerk Member ✭✭
    edited June 25

    It works when changed the code below. But app started one second and off

    Bootupreceiver

    Intent i = new Intent(context, typeof(MainActivity));
    i.AddFlags(ActivityFlags.NewTask);
    context.StartActivity(i);

  • NigelWebberNigelWebber USMember ✭✭✭

    Not sure then, I have not tried to launch an actual UI at boot, only a service - I think auto-launching a UI would annoy my users a fair amount!

    You might want to check this out: https://www.digi.com/resources/documentation/digidocs/90001546/task/android/t_faq_autostart_custom_android_applications.htm

    Check that the manifest entries match yours - the above also shows how to replace the launcher with your app. Beware that if stuff works on Android 7, there is no guarantee it will work on 6,9 etc. The whole services thing and restrictions around it seems to get changed willy-nilly every release.

  • ByBerkByBerk Member ✭✭
    edited June 25

    Hi @NigelWebber

    public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
    {
    Intent resultIntent = new Intent(StaticDefs.Com_Spacelabs_ProteusPatientApp_Android_SwitchScreenIntent);
    resultIntent.PutExtra(PageId.PageIdStringIdent, (int)PageId.RequestedPageId.PatientEventListScreen);
    resultIntent.SetFlags(ActivityFlags.NoHistory | ActivityFlags.NewTask | ActivityFlags.SingleTop);

    I don't understand the your code here. Intent and pageid. What should i write? MainActivity?

  • NigelWebberNigelWebber USMember ✭✭✭

    Yep, this is just something specific to my own app. I pass stuff into MainActivity via the extra's. I then use these to decide what xamarin.forms page should be loaded.

    You need:

    Intent resultIntent = new Intent(Application.Context, MainActivity);

    You should not need the extra's (these are a way of feeding parameters into the intent, for extraction by the destination. Note that my flags indicate SingleTop, NoHistory, this might not be what you want.

    Nigel

  • ByBerkByBerk Member ✭✭

    Hi @NigelWebber

    Thank you for all your help.So my project won't be exactly what I want.

    I think not working xamarin forms messaging center and essential secure storage half starting. I think only working on runtime . do you agree?

    I wonder sqlite works on half starting? do you know?

  • NigelWebberNigelWebber USMember ✭✭✭

    Hi

    For best practice, try to avoid the use of the MessagingCenter, it might seem like an easy way out, but it quickly becomes unmanageable. In my application, the classes running under my service simply fire events that the viewmodel subscribes to, you can do these subs/unsubs via either the dependency service, or, for platform-independent shared code (in my case, my BleComms class), by accessing via a singleton.

    The MessagingCenter works fine in a service ditto the DependencyService (both require Forms.Init to have been called). I use the DependencyService extensively.

    My service uses both SharedPrefs (Tip get Xamarin.Essentials nuget - makes it easy). And also SqlLite (via the Nuget). These both work just fine from the boot start service, which gets the MAC address for the previously connected BLE pheriperal, reconnects then reloads the existing database.

    Note that I had issues using Application.Context.GetSharedPreferences (ISharedPreferences) from my Service (even after having called forms.init. Initially, I rolled my own interface and implemented an android and ios-specific version accessed via an interface, before switching initially to Xam.Plugins.Settings and then to Xamarin.Essentials (which has basically incorporated the former). This works fine from a service, but needs you to call Xamarin.Essentials.Platform.Init(this, bundle); in an activity after forms.init

    See also secure storage in Xamarin.Essentials: https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android

    Make sure you have the permissions, and if it crashes, look at the logs - they will tell you whats missing. Note that some Android permissions must be requested before use (the end user will be prompted to accept), there is no way in newer versions of android to get these auto-allocated via the manifest (although they still need to be listed in the manifest).

     public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
        {
    protected override void OnCreate(Bundle bundle)
        {
            ......
            Task.Run(async () => await getRequiredPermissions(_permissionRequests));
        }
    
    
         readonly string[] _permissionRequests =
                {
                    Manifest.Permission.AccessCoarseLocation,
                    Manifest.Permission.Nfc,
                };
    
        async Task getRequiredPermissions(String[] permissions)
                {
                    var permissionsWeDontHave = new List<String>();
    
                    foreach (var permission in permissions)
                    {
                        if (CheckSelfPermission(permission) != (int)Permission.Granted)
                            permissionsWeDontHave.Add(permission);
                    }
    
                    //Check if we already have all we need
                    if (!permissionsWeDontHave.Any())
                        return;
    
                    bool fShowAlert = false;
                    foreach (var permission in permissionsWeDontHave)
                    {
                        if (ShouldShowRequestPermissionRationale(permission))
                            fShowAlert = true;
                    }
    
                    if (fShowAlert)
                    { 
                        AlertDialog.Builder builder = new AlertDialog.Builder(this);
                        builder.SetTitle("App Requires Permissions");
                        builder.SetMessage("To Perform its function, app requires the permissions that follow to be granted!");
                        builder.Show();
                    }
    
                    RequestPermissions(permissionsWeDontHave.ToArray(), PermissionsRequiredId);
                }
    }
    

    I was fairly seasoned as a C#, C++ and firmware developer (30 years) when I came to Xamarin, but MVVM was alien to me, and Xamarin appeared to just be a barrier. Its a steep learning curve. I developed my app for Android initially, but was able to get up up/running on iOS within 2 days. This despite the fact that this is not the usual simple app accessing some online database for content, but rather includes BLE Comms, NFC for pairing etc. It helped that I also developed the other end of the BLE connection on an NRF52832 so I was responsible for both ends. Xamarin's strengths lie in code re-use. I not only have shared libraries for my Apps, but libraries that I share between native windows applications and the xamarin app, My entire comms protocol for instance was implemented as a .net2.0 class lib and is used everywhere via that same lib, over BLE and USB and only needs unit-testing the once.

    Nigel

Sign In or Register to comment.