When you close the app by swiping it in recent apps, it will cancel any services and terminate most aspects of the app gracefully. However, if there are any notifications that were SetOngoing(true), then these will remain if the app suddenly is closed, and there aren't any services that listen for the app's termination.
What is the right way to deal with this problem?
Recently, I coded a music player, and I arranged it such that in the OnStop for my activities, the notification is canceled (and so is the thread updating the progress bar within it). Then, OnResume, I trigger the notification again.
If they "recent apps swipe" it away, or click away, the notification goes away now, as long as the music isn't playing. So to get rid of the notification, you have to pause it, and then swipe away. Otherwise, there is a leak memory if the app is closed by swipe, where the notification remains open and is buggy afterwards if the app is reopened, and the app crashes if you click the notification (though maybe that's because I can't figure out how to get started with saved state bundles). Likewise, there is a problem if you let the app close the notification every OnStop, as then it will be closed as the user does other things with their phone, even though the music is playing (which sort of defeats the point of it right?)
Are there other better ways to handle this? Who has a good saved state bundle if that is indeed relevant to my issue?
Thanks for the discussion
You can cancel the notification when android App is closed by swipe with the following code:
[Service]
public class ForegroundServiceDemo : Service
{
public override void OnTaskRemoved(Intent rootIntent)
{
//this.StopSelf();
//this.StopForeground(StopForegroundFlags.Remove);
this.StopService(new Intent(this,typeof(ForegroundServiceDemo)));
base.OnTaskRemoved(rootIntent);
}
}
By overriding the OnTaskRemoved method of the service, the system will call this method when user closes the app by swipe. And each of the three lines code can cancel the notification and stop the service when the app is closed by swipe.
I found this, finally, after trying every search terms imaginable, and wow there is a whole section on this. I do not have it working yet, but I can report back with code when I do. Here is the solution: https://developer.android.com/guide/topics/media-apps/media-apps-overview
Seems you have to implement the media player service as a specific kind of service that registers to the notification. I am in the process of refactoring the heart of my code, which perhaps should be terrifying, but feels more like the final algorithm on a Rubix's cube... I will report back in like 10 work hours with some working code (I hope).
Thanks to everyone contributing on this discussion!
OK, so, after much dabbling and dozens of work hours... I have found the best way to handle this issue is to create a MediaBrowserService with a MediaSession. In the notification creation code, it is very particular about how you start that notification (which has to be in the Foreground and bound to the MediaSession). Once this is done, the notification will stay open, even if you close the app, and clicking it will always bring you back to the activity bound to the service (see the supplied code below). Then, you just have a button on the notification to close itself and the app. Voila, a notification that does NOT remain open if the app is closed from the recent apps, etc.
public static void CancelNotificationBreadCrumb()
{
if (cts != null)
{
cts.Cancel();
Thread.Sleep(250);
// Cancellation should have happened, so call Dispose
cts.Dispose();
MyLogger.Debug("MyMediaPlayer: CloseEntireApp: Notification should have been disposed.");
}
}
public static void NotificationNowPlayingBreadCrumb()
{
try
{
Intent intent = MenuManager.GetGoToNowPlayingIntent(context, GetCurrentlyPlaying());
manager = (NotificationManager)context.GetSystemService(NotificationService);
PendingIntent pendingIntent = PendingIntent.GetActivity(context, 1, intent, PendingIntentFlags.Immutable);
NotificationChannel notificationChannel = new NotificationChannel(ChannelId, ChannelId, NotificationImportance.Low);
notificationChannel.EnableLights(false);
notificationChannel.EnableVibration(false);
notificationChannel.SetSound(null, null);
//notificationChannel.SetVibrationPattern(new long[] { 10, 20 });
manager.CreateNotificationChannel(notificationChannel);
Notification notification = NowPlayingAdapter.InflateNotification(context, currentFile, ChannelId, pendingIntent);
service.StartForeground(MY_MEDIA_NOTIFICATION_ID, notification);
manager.Notify(MY_MEDIA_NOTIFICATION_ID, notification);
// Then trigger the thread to update the real-time features
if (cts == null || cts.IsCancellationRequested)
cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(new WaitCallback(RunInBackground), cts.Token);
} catch(Exception e)
{
string message = "MyMediaPlayer: NotificationNowPlayingBreadCrumb: Could not create now playing breadcrumb notification; message: " + e.Message;
MyLogger.Error(message);
}
}
public static void CloseEntireApp()
{
MyLogger.Trace("MyMediaPlayer: Entering CloseEntireApp...");
if (player != null)
player.Release();
CancelNotificationBreadCrumb();
MediaReceiver.Dispose();
MediaSession.Dispose();
MyLogger.Trace("MyMediaPlayer: CloseEntireApp is Killing App. Good bye!");
service.StopSelf();
Android.OS.Process.KillProcess(Android.OS.Process.MyPid());
}
Here is the OnCreate method for my service:
public class MyMediaPlayer : MediaBrowserServiceCompat
{
private static MediaPlayer? player;
private static MusicAppFile? currentFile;
private static List<MusicAppFile>? allFilesInCurrentContext;
private static Context? context;
private static List<int> recentIndexes = new List<int>();
private static int maxRecentIndexes = 30;
private static bool shuffleMode = false;
private static ViewGroup? Parent;
private static NotificationManager? manager;
private static CancellationTokenSource? cts;
public static MediaButtonReceiver? MediaReceiver;
public static MediaSessionCompat? MediaSession;
private static PlaybackStateCompat.Builder stateBuilder;
private static MediaBrowserServiceCompat service;
public IBinder Binder { get; private set; }
public const string ActionPlay = "com.xamarin.action.PLAY";
public const string ActionPause = "com.xamarin.action.PAUSE";
public const string ActionNext = "com.xamarin.action.NEXT";
public const string ActionStop = "com.xamarin.action.STOP";
public const string ActionBack = "com.xamarin.action.BACK";
public const string ActionCloseApp = "com.xamarin.action.CLOSEAPP";
public static string ChannelId = "NowPlayingNote";
public static string MY_MEDIA_ROOT_ID = "media_root_id";
public static int MY_MEDIA_NOTIFICATION_ID = 1111111;
public static string MY_MEDIA_TAG = "media_tag";
public override void OnCreate()
{
base.OnCreate();
// Create a MediaSessionCompat
MediaSession = new MediaSessionCompat(context, MY_MEDIA_TAG);
// Enable callbacks from MediaButtons and TransportControls
MediaSession.SetFlags(
MediaSessionCompat.FlagHandlesMediaButtons |
MediaSessionCompat.FlagHandlesTransportControls);
// Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
stateBuilder = new PlaybackStateCompat.Builder()
.SetActions(
PlaybackStateCompat.ActionPlay |
PlaybackStateCompat.ActionPlayPause |
PlaybackStateCompat.ActionSkipToNext |
PlaybackStateCompat.ActionSkipToPrevious |
PlaybackStateCompat.ActionStop);
MediaSession.SetPlaybackState(stateBuilder.Build());
// MySessionCallback() don't do this. C# isn't as good at doing callbacks because you can't define them inline
// MediaSession.SetCallback(new MediaSessionCallback(this));
service = this;
// Set the session's token so that client activities can communicate with it.
SessionToken = MediaSession.SessionToken;
}
...
I create this service when they click to select a file in one of the menu activities (so in a method called by a method called by an OnClick delegate):
if (musicMenu != null)
{
bool stillPlayingSameFile = MyMediaPlayer.UpdateCurrentContext(c, musicMenu, mf);
if (cts == null)
{
// Start the service and tell it to call play
InitiateMediaBrowserService(c);
} else
{
MyMediaPlayer.Play(stillPlayingSameFile);
}
}
GoToNowPlaying(c, mf);
and the inner service there:
public static void InitiateMediaBrowserService(Context c)
{
// Start the service and tell it to call play
Intent intent = new Intent(c, typeof(MyMediaPlayer));
intent.SetAction(MyMediaPlayer.ActionPlay);
cts = new CancellationTokenSource();
Platform.AppContext.StartForegroundService(intent);
}
Ok, so now the play service, which is triggered from the action play intent here, and makes the call to start the notification, which is where the StartForeground call is made (see the first snippet at the top):
public static void Play(bool stillPlayingSameFile)
{
// If the player has not been created before, or it is a new track, then it needs to be recreated
if (player == null || !stillPlayingSameFile)
{
// If we're here to recreate the player, destroy the old one in memory first
if (player != null)
player.Release();
// Then add the new player
if (currentFile != null)
{
Uri uri = Android.Net.Uri.Parse(currentFile.FilePath);
MediaPlayer media = MediaPlayer.Create(context, uri);
media.Completion += OnCompletion;
if (MediaReceiver == null)
MediaReceiver = new MediaButtonReceiver(context);
media.RoutingChanged += MediaReceiver.OnRoutingChanged;
player = media;
player.SetWakeMode(context, WakeLockFlags.Partial);
}
// Finally, add this file to the list of those recently played
int indexToPlay = allFilesInCurrentContext.IndexOf(currentFile);
if (indexToPlay >= 0)
recentIndexes.Add(indexToPlay);
if (recentIndexes.Count > maxRecentIndexes)
recentIndexes.RemoveAt(0);
}
// Finally start the player, which picks up where left off if this is the same track
if (!IsPlaying() || !stillPlayingSameFile)
{
player.Start();
NotificationNowPlayingBreadCrumb();
}
}
The MediaButtonReceiver and MediaBroadcastReceiver classes are pretty straightforward, so comment if you really need that code. One other thing to note is that you do have to bind the service to an activity (I suggest the now playing activity):
protected override void OnStart()
{
base.OnStart();
//Config.ConfigureBluetoothIntegration(this); TODO remove this
Intent serviceToStart = new Intent(this, typeof(MyMediaPlayer));
//serviceToStart.SetAction(MyMediaPlayer.ActionPlay);
BindService(serviceToStart, new ServiceConnection(this), Bind.AutoCreate);
}
So there, now there IS an example of how to use the MediaSession and MediaSessionCompat and MediaBrowserServiceCompat online somewhere. Even ChatGPT could not find an example or tell me how to do this. You are welcome, internet. Enjoy your coding!
I'm having a problem with my app during the start up. I'm getting at exception that says
A method was called at an unexpected time. Could not create a
new view because the main window has not yet been created
First I display a splash screen so I can get some data from the internet in the background. My splash screen works fine and I implemented it correctly as indicated in the documentation.
In App.xaml.ca I have some standard code for splash screen
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
...
if (e.PreviousExecutionState != ApplicationExecutionState.Running)
{
bool loadState = (e.PreviousExecutionState == ApplicationExecutionState.Terminated);
ExtendedSplash extendedSplash = new ExtendedSplash(e.SplashScreen, loadState);
Window.Current.Content = extendedSplash;
}
...
Window.Current.Activate();
}
Then in my App constructor I have this
public static Notifications notifications;
public App()
{
Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync(
Microsoft.ApplicationInsights.WindowsCollectors.Metadata |
Microsoft.ApplicationInsights.WindowsCollectors.Session);
this.InitializeComponent();
this.Suspending += OnSuspending;
SomeClass.RunTasks(); //acquire data from a REST service
//initializing the object for subscribing to push notification, not sure if this is the best place to put this.
App.notifications = new Notifications("hubname", "myEndpoint");
}
The exception occurs inside my RunTasks() method which looks like this
public class SomeClass
{
GetHTTPResponse _aggregateData = new GetHTTPResponse("http://someRestService");
public async void RunTasks()
{
try
{
HttpResponseMessage aggregateData = await _aggregateData.AcquireResponse();
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
() =>
{
//do a bunch of stuff with the data
//NOTE: I am making updates to my ViewModel here with the data I acquired
//for example App.ViewModel.Time = somevalue
//when finished dismiss the splash screen
ExtendedSplash.Instance.DismissExtendedSplash();
}
);
}
catch (System.Exception ex)
{
}
}
}
Any ideas how I can improve this and eliminate the error?
Could it have something to do with me updating my ViewModel items (which are data bound to UI components)?
EDIT when I remove the creation of my notifications object from App.cs constructor (and move it into the RunTasks() method, the error goes away.
App.notifications = new Notifications("hubname", "myEndpoint");
The reason you get the exception is because Windows.ApplicationModel.Core.CoreApplication.MainView is not valid in your application's constructor as the main view has not been created yet.
You can access it once you have received the Application.OnLaunched/OnActivated event.
Thanks!
Stefan Wick -
Windows Developer Platform
I am making a UWP App where I run Background Audio in the MainPage on a Button Click event. When I move to another page, there's also a different Media to play in Background Audio Task there.
How can I stop the currently playing Task to run the other? Should I define something globally? Any help regarding this issue?
Edit
I am using this sample: https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/BackgroundAudio While the backgroundAudio of the first Page is running, I go to the second page and on a click event I set a new List with the following code:
// First update the persisted start track
ApplicationSettingsHelper.SaveSettingsValue(ApplicationSettingsConstants.TrackId, RadioFacade.mySongs[0].MediaUri.ToString()); //here
ApplicationSettingsHelper.SaveSettingsValue(ApplicationSettingsConstants.Position, new TimeSpan().ToString());
// Start task
StartBackgroundAudioTask();
But the new song takes more than the estimated time to run and enter the else of this method:
private void StartBackgroundAudioTask()
{
AddMediaPlayerEventHandlers();
var startResult = this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
bool result = backgroundAudioTaskStarted.WaitOne(10000);
//Send message to initiate playback
if (result == true)
{
MessageService.SendMessageToBackground(new UpdatePlaylistMessage(RadioFacade.mySongs));
MessageService.SendMessageToBackground(new StartPlaybackMessage());
}
else
{
throw new Exception("Background Audio Task didn't start in expected time");
}
});
startResult.Completed = new AsyncActionCompletedHandler(BackgroundTaskInitializationCompleted);
}
and the old (first playing) song keeps playing.
I tried to Stop the current BackgroundMediaPlayer using BackgroundMediaPLayer.Shutdown() but it didn't work.
Any idea how to let the old song stop and the current song play?
You can control the background media player by sending messages to it from the foreground. For example,
From the foreground app:
BackgroundMediaPlayer.SendMessageToBackground(new ValueSet
{
{"playnew", "some value"}
});
In your background task:
public sealed class AudioPlayer : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
BackgroundMediaPlayer.MessageReceivedFromForeground += BackgroundMediaPlayer_MessageReceivedFromForeground;
...
...
}
private async void BackgroundMediaPlayer_MessageReceivedFromForeground(object sender, MediaPlayerDataReceivedEventArgs e)
{
object value;
if (e.Data.TryGetValue("playnew", out value) && value != null)
{
// value will be whatever you pass from the foreground.
BackgroundMediaPlayer.Current.Pause();
BackgroundMediaPlayer.Current.Source = stream source;
BackgroundMediaPlayer.Current.Play();
}
}
...
}
"playnew" can be a global constant in your application. You can use the ValueSet to pass the live stream url to the background task as the value.
I am working with NFC using Xamarin Android.
My scenario is to read nfc tag. I have implemented the following, which works using a button. But I would like it so the user doesn't have to press Scan button to scan nfc tag.
OnCreate
scanButton.Click += (object sender, EventArfs e) =>{
var view = (View)sender;
if(view == Resource.Id.scan){
var mesgEl = FindViewById<TextView>(Resource.Id.msg);
msgEl.Text = "Ready to Scan. Touch and hold the tag against the phone.";
InitNfcScanner();
}
}
InitNfcScanner
private void InitialiseNfcScanner(){
// Create an intent filter for when an NFC tag is discovered. When
// the NFC tag is discovered.
var tagDetected = new IntentFilter(NfcAdapter.ActionTagDiscovered);
var filters = new[] { tagDetected };
// When an NFC tag is detected, Android will use the PendingIntent to come back to this activity.
// The OnNewIntent method will invoked by Android.
var intent = new Intent(this, GetType()).AddFlags(ActivityFlags.SingleTop);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
if (_nfcAdapter == null) {
var alert = new AlertDialog.Builder (this).Create ();
alert.SetMessage ("NFC is not supported on this device.");
alert.SetTitle ("NFC Unavailable");
alert.SetButton ("OK", delegate {
// display message here
});
alert.Show ();
} else {
_nfcAdapter.EnableForegroundDispatch (this, pendingIntent, filters, null);
}
}
OnNewIntent
protected override void OnNewIntent(Intent intent)
{
// onResume gets called after this to handle the intent
Intent = intent;
}
OnResume()
protected override void OnResume ()
{
base.OnResume ();
InitialiseNfcScanner();
if (NfcAdapter.ActionTagDiscovered == Intent.Action) {
// do stuff
}
}
But if i remove the button delegate from OnCreate, and call InitNfcScanner(), I get the error Unable to start activity: java.lang.illegalStateException: Foreground dispatch can only be enabled when your activity is resumed.
I want the user to be able to just scan the asset, once the activity is loaded. What would be a good solution to achieve this?
Can you please add the button delegate in OnCreate and Just call button.PerformClick() in OnResume.
I have now resolved this issue.
The objective was to be able to read nfc tag without pressing Button.
So i removed the button from the view, and I removed the ScanButton delegate from OnCreate.
As I was calling InitialiseNfcScanner inside the OnResume(), this is all i needed, because _nfcAdapter.EnableForegroundDispatch (this, pendingIntent, filters, null);
would create a pendingIntent and looking at the Android Guidelines for this, the ForgroundDispatch can only been called from the OnResume().
See http://developer.android.com/reference/android/nfc/NfcAdapter.html
enableForegroundDispatch
I have a Windows Phone 8.1 Universal App that I am working on adding basic Cortana support to. A lot of the articles about this are for Silverlight etc. - I'm finding it hard to find really good information about this.
So far, I have activation working if the app is already running or suspended. However, if the app is completely exited, then upon activation it crashes immediately. I've tried using Hockey and a simple "LittleWatson" routine to catch the crash, but it seems to happen too soon to be caught. I've seen some references to doing a private beta and trying to get the crash dump, but I didn't have any luck with that so far.
Here's what my activation code looks like in app.xaml.cs:
protected override void OnActivated(IActivatedEventArgs args) {
base.OnActivated(args);
ReceivedSpeechRecognitionResult = null;
if (args.Kind == ActivationKind.VoiceCommand) {
var commandArgs = args as VoiceCommandActivatedEventArgs;
if (commandArgs != null) {
ReceivedSpeechRecognitionResult = commandArgs.Result;
var rootFrame = Window.Current.Content as Frame;
if (rootFrame != null) {
rootFrame.Navigate(typeof(CheckCredentials), null);
}
}
}
}
and here is my check for the command result:
private async Task CheckForVoiceCommands() {
await Task.Delay(1); // not sure why I need this
var speechRecognitionResult = ((App)Application.Current).ReceivedSpeechRecognitionResult;
if (speechRecognitionResult == null) {
return;
}
var voiceCommandName = speechRecognitionResult.RulePath[0];
switch (voiceCommandName) {
// omitted
}
((App)Application.Current).ReceivedSpeechRecognitionResult = null;
}
I'm pretty sure from inserting messages etc. that it fails long before it gets this far.
There's likely something easy I'm missing but I don't know what...
What is causing the crash so early?
EDIT One thing I tried is using the "debug without launch" configuration to try to catch the exception. When I do this, the app appears to hang forever connected in the debugger on the splash screen. However, that did let me force a break. It hangs in
global::Windows.UI.Xaml.Application.Start((p) => new App());
which as best I can tell, just tells me the app is hanging somewhere. That's the only line in the call stack.
Copy a segment of the OnLaunched code into OnActivated like in the example below. OnLaunched is not called when the App is Activated and it does some essential work like activating the window.
protected override void OnActivated(IActivatedEventArgs args)
{
// When a Voice Command activates the app, this method is going to
// be called and OnLaunched is not. Because of that we need similar
// code to the code we have in OnLaunched
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
rootFrame.CacheSize = 1;
Window.Current.Content = rootFrame;
rootFrame.Navigate(typeof(MainPage));
}
Window.Current.Activate();
// For VoiceCommand activations, the activation Kind is ActivationKind.VoiceCommand
if(args.Kind == ActivationKind.VoiceCommand)
{
// since we know this is the kind, a cast will work fine
VoiceCommandActivatedEventArgs vcArgs = (VoiceCommandActivatedEventArgs)args;
// The NavigationTarget retrieved here is the value of the Target attribute in the
// Voice Command Definition xml Navigate node
string target = vcArgs.Result.SemanticInterpretation.Properties["NavigationTarget"][0];