Hi I am trying to build an application in xamarin forms using PCL. I am trying to logout user from my app if the app is idle more than 10minute or more. I tried it by events
Unfortunately this is not really something that you can easily do on the client side. There is also no way to do it just from the PCL. There might be a plugin that you can add to your project, but I have not found one yet.
The reason for this is the difference in the way that iOS and Android handle the app life cycle. Both are very different. For instance once iOS suspends your application, there is really only 2 ways to wake it up. GPS location update and Push notification. In Android it is easier as they have the AlarmManager that you can register and intent with to do the logout for you.
My suggestion would be, if you control the api you are using, have the session expire server side so that any request that comes in after that 10 minutes would fail, and handle those failures appropriately on the client side.
If your concern is purely for when the app is in the foreground and active but not used, you will then have to implement a timer, and reset it every time there is some user interaction.
For now I use the following approach. Might need to do some additional testing to make sure everything works as expected. For example I am not sure what will happen if the app (iOS or Android) is in the background for extended amounts of time. Will the timer still be called every second or not? Perhaps when using timers with a short enough expiration time (~5 minutes) this is no issue whatsoever? Etc...
I've based my approach on several pieces of code I found on the web (some Xamarin code, some Swift / Java code) - there didn't seem to be a good comprehensive solution.
Anyways, some preliminary testing suggests this approach works fine.
First I've created a singleton class called SessionManager
. This class contains a timer (actually just a while loop that sleeps every second) and methods to start, stop and extend the timer. It will also fire an event if the session expiration timer is expired.
public sealed class SessionManager
{
static readonly Lazy<SessionManager> lazy =
new Lazy<SessionManager>(() => new SessionManager());
public static SessionManager Instance { get { return lazy.Value; } }
SessionManager() {
this.SessionDuration = TimeSpan.FromMinutes(5);
this.sessionExpirationTime = DateTime.FromFileTimeUtc(0);
}
/// <summary>
/// The duration of the session, by default this is set to 5 minutes.
/// </summary>
public TimeSpan SessionDuration;
/// <summary>
/// The OnSessionExpired event is fired when the session timer expires.
/// This event is not fired if the timer is stopped manually using
/// EndTrackSession.
/// </summary>
public EventHandler OnSessionExpired;
/// <summary>
/// The session expiration time.
/// </summary>
DateTime sessionExpirationTime;
/// <summary>
/// A boolean value indicating wheter a session is currently active.
/// Is set to true when StartTrackSessionAsync is called. Becomes false if
/// the session is expired manually or by expiration of the session
/// timer.
/// </summary>
public bool IsSessionActive { private set; get; }
/// <summary>
/// Starts the session timer.
/// </summary>
/// <returns>The track session async.</returns>
public async Task StartTrackSessionAsync() {
this.IsSessionActive = true;
ExtendSession();
await StartSessionTimerAsync();
}
/// <summary>
/// Stop tracking a session manually. The OnSessionExpired will not be
/// called.
/// </summary>
public void EndTrackSession() {
this.IsSessionActive = false;
this.sessionExpirationTime = DateTime.FromFileTimeUtc(0);
}
/// <summary>
/// If the session is active, then the session time is extended based
/// on the current time and the SessionDuration.
/// duration.
/// </summary>
public void ExtendSession()
{
if (this.IsSessionActive == false) {
return;
}
this.sessionExpirationTime = DateTime.Now.Add(this.SessionDuration);
}
/// <summary>
/// Starts the session timer. When the session is expired and still
/// active the OnSessionExpired event is fired.
/// </summary>
/// <returns>The session timer async.</returns>
async Task StartSessionTimerAsync() {
if (this.IsSessionActive == false) {
return;
}
while (DateTime.Now < this.sessionExpirationTime) {
await Task.Delay(1000);
}
if (this.IsSessionActive && this.OnSessionExpired != null) {
this.IsSessionActive = false;
this.OnSessionExpired.Invoke(this, null);
}
}
}
For the Android app I then:
Configure the SessionManager
in the MainActivity to logout when the session expires.
Override the OnUserInteraction
method in the MainActivity
to extend the session timer on user interaction.
public class MainActivity /* ... */ {
protected override void OnCreate(Bundle bundle)
{
// ...
SessionManager.Instance.SessionDuration = TimeSpan.FromSeconds(10);
SessionManager.Instance.OnSessionExpired = HandleSessionExpired;
}
public override void OnUserInteraction()
{
base.OnUserInteraction();
SessionManager.Instance.ExtendSession();
}
async void HandleSessionExpired(object sender, EventArgs e)
{
await App.Instance.DoLogoutAsync();
}
}
For iOS I do the following:
Configure the SessionManager
in the AppDelegate to logout when the session expires.
Add a custom gesture handler to the key window to extend the session timer on user interaction.
public partial class AppDelegate /* ... */
{
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
// ...
var success = base.FinishedLaunching(app, options);
if (success) {
SessionManager.Instance.SessionDuration = TimeSpan.FromSeconds(10);
SessionManager.Instance.OnSessionExpired += HandleSessionExpired;
var allGesturesRecognizer = new AllGesturesRecognizer(delegate
{
SessionManager.Instance.ExtendSession();
});
this.Window.AddGestureRecognizer(allGesturesRecognizer);
}
return success;
}
async void HandleSessionExpired(object sender, EventArgs e)
{
await App.instance.DoLogoutAsync();
}
class AllGesturesRecognizer: UIGestureRecognizer {
public delegate void OnTouchesEnded();
private OnTouchesEnded touchesEndedDelegate;
public AllGesturesRecognizer(OnTouchesEnded touchesEnded) {
this.touchesEndedDelegate = touchesEnded;
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
this.State = UIGestureRecognizerState.Failed;
this.touchesEndedDelegate();
base.TouchesEnded(touches, evt);
}
}
}
Edit: Bolo asked a good question below, so I'll add it here. StartTrackSessionAsync is called as soon as the user is logged in. EndTrackSession should be called when the user is logged out of the app as well of course.
I was able to use the Device.StartTimer in Xamarin Forms to create an expiration. For my app the user switched screens fairly often so I would cause inactivity to be reset between screen transitions. It was a little less obnoxious then tying the method to each button press and screen tap. The class that houses the logic looks something like this:
public class InactivityService
{
public ActivityMonitorService( )
{
}
public DateTime LastClick { get; private set; }
public TimeSpan MaxLength { get; private set; }
public void Start(TimeSpan maxDuration, Action expirationCallback = null)
{
MaxLength = maxDuration;
Notify();
_expirationCallBack = expirationCallback;
ResetTimer();
}
public void Notify()
{
LastClick = DateTime.Now;
}
public void Stop()
{
}
public TimeSpan TimeSinceLastNotification()
{
var now = DateTime.Now;
var timeSinceLastClick = now - LastClick;
return timeSinceLastClick;
}
public TimeSpan GetNewTimerSpan()
{
var newDuration = MaxLength - TimeSinceLastNotification();
return newDuration;
}
public bool IsExpired(DateTime time)
{
return time - LastClick > MaxLength;
}
private bool CallBack()
{
if (IsExpired(DateTime.Now))
{
Expire();
}
else
{
ResetTimer();
}
return false;
}
public async void Expire()
{
if (_expirationCallBack != null)
_expirationCallBack.Invoke();
Stop();
//Notify user of logout
//Do logout navigation
}
private void ResetTimer()
{
Device.StartTimer(GetNewTimerSpan(), CallBack);
}
}
Tweaked @Wolfgang version
public sealed class SessionManager
{
static readonly Lazy<SessionManager> lazy =
new Lazy<SessionManager>(() => new SessionManager());
public static SessionManager Instance { get { return lazy.Value; } }
private Stopwatch StopWatch = new Stopwatch();
SessionManager()
{
SessionDuration = TimeSpan.FromMinutes(5);
}
public TimeSpan SessionDuration;
public void EndTrackSession()
{
if (StopWatch.IsRunning)
{
StopWatch.Stop();
}
}
public void ExtendSession()
{
if (StopWatch.IsRunning)
{
StopWatch.Restart();
}
}
public void StartTrackSessionAsync()
{
if (!StopWatch.IsRunning)
{
StopWatch.Restart();
}
Xamarin.Forms.Device.StartTimer(new TimeSpan(0, 0, 2), () =>
{
if (StopWatch.IsRunning && StopWatch.Elapsed.Minutes >= SessionDuration.Minutes)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await Prism.PrismApplicationBase.Current.Container.Resolve<INavigationService>().NavigateAsync("/Index/Navigation/LoginPage");
});
StopWatch.Stop();
}
return true;
});
}
}
Under main activity added the below
public override void OnUserInteraction()
{
base.OnUserInteraction();
SessionManager.Instance.ExtendSession();
}