Skip to content

Commit

Permalink
rebase and copy softlion's work.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hackmodford committed Aug 17, 2021
1 parent fbe914f commit de75513
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 5 deletions.
9 changes: 9 additions & 0 deletions MvvmCross/Platforms/Android/Core/MvxAndroidSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
namespace MvvmCross.Platforms.Android.Core
{
#nullable enable

public interface IMvxActivityLifecycleCallbacksProvider
{
IMvxAndroidCurrentTopActivity AndroidCurrentTopActivityFinder { get; }
}

public abstract class MvxAndroidSetup
: MvxSetup, IMvxAndroidGlobals, IMvxAndroidSetup
{
Expand Down Expand Up @@ -78,6 +84,9 @@ protected virtual void InitializeAndroidCurrentTopActivity(IMvxIoCProvider iocPr

protected virtual IMvxAndroidCurrentTopActivity CreateAndroidCurrentTopActivity()
{
if (MvxAndroidApplication.Instance is IMvxActivityLifecycleCallbacksProvider provider && provider.AndroidCurrentTopActivityFinder != null)
return provider.AndroidCurrentTopActivityFinder;

var mvxApplication = MvxAndroidApplication.Instance;
if (mvxApplication != null)
{
Expand Down
32 changes: 31 additions & 1 deletion MvvmCross/Platforms/Android/Views/MvxAndroidApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@

namespace MvvmCross.Platforms.Android.Views
{
public abstract class MvxAndroidApplication : Application, IMvxAndroidApplication
public abstract class MvxAndroidApplication : Application, IMvxAndroidApplication, IMvxActivityLifecycleCallbacksProvider
{
public static MvxAndroidApplication Instance { get; private set; }

/// <summary>
/// Using the top activity discovered by MvxStartupLifecycleCallback works in more situations than using the old MvxApplicationCallbacksCurrentTopActivity.
/// But the IoCProvider is not yet available. So make it discoverable by MvxAndroidSetup which will register it.
/// As CreateActivityLifecycleObserver can be overriden, the resulting IActivityLifecycleCallbacks may not implement IMvxAndroidCurrentTopActivity. Make sure this is handled.
/// </summary>
public IMvxAndroidCurrentTopActivity AndroidCurrentTopActivityFinder => activityLifecycle as IMvxAndroidCurrentTopActivity;

protected virtual IActivityLifecycleCallbacks CreateActivityLifecycleObserver() => new MvxStartupLifecycleCallback();

private IActivityLifecycleCallbacks activityLifecycle;

public MvxAndroidApplication()
{
Instance = this;
Expand All @@ -30,6 +41,25 @@ public MvxAndroidApplication(IntPtr javaReference, JniHandleOwnership transfer)
protected virtual void RegisterSetup()
{
}

public override void OnCreate()
{
base.OnCreate();

activityLifecycle = CreateActivityLifecycleObserver();
RegisterActivityLifecycleCallbacks(activityLifecycle);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
UnregisterActivityLifecycleCallbacks(activityLifecycle);
activityLifecycle.Dispose();
}

base.Dispose(disposing);
}
}

public abstract class MvxAndroidApplication<TMvxAndroidSetup, TApplication> : MvxAndroidApplication
Expand Down
325 changes: 325 additions & 0 deletions MvvmCross/Platforms/Android/Views/MvxStartupLifecycleCallback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.OS;
using Android.Runtime;
using MvvmCross.Core;
using MvvmCross.Platforms.Android.Core;
using MvvmCross.ViewModels;
using MvvmCross.Views;

namespace MvvmCross.Platforms.Android.Views
{
/// <summary>
/// Would be good to move this into core
/// </summary>
public enum ApplicationState
{
Unknown,
Deactivation, //Active => inactive
Background, //Start running in background (either from foreground or inactive state)
Activation, //Inactive => active
Foreground, //Background => Foreground
}

/// <summary>
/// Mark all startup activities with this interface.
/// A startup activity may be launched directly by Android using an intent filter.
/// </summary>
public interface IStartupActivity
{
/// <summary>
/// Called after the startup lifecycle has finished.
/// The startup activity, whichever it is, can control what it should do in this method.
/// </summary>
/// <remarks>
/// <para>
/// Typical usage 1:
/// if a startup activity is the "[MainActivity]" and is a splash screen which don't have a corresponding viewmodel,
/// close it by calling Finish() in this method. Otherwise leave blank.
/// </para>
/// <para>
/// Typical usage 2:
/// if a startup activity is started by an intent filter and is NOT the mainActivity, it won't have a corresponding viewmodel.
/// close it by calling Finish() in this method. Otherwise leave blank.
/// </para>
/// <para>Note that by default any activity can be started by another app using an intent filter targeting your app. If this happens and the activities are not decorated with IStartupActivity, mvvmcross won't work as expected.</para>
/// </remarks>
void FinishActivity();
}

/// <summary>
/// You MUST mark Activities that can shut downs the app with IShutdownActivity.
/// Otherwise opening the app again by tapping its icon will display nothing, and tapping the icon again will only display the splash screen.
/// </summary>
/// <remarks>
/// An activity can shut downs the app if it is the last activity that can be displayed by the app.
/// ie: if the user taps "back" while this activity is displayed, the app is shut down.
/// </remarks>
public interface IShutdownActivity
{
}

public class MvxStartupLifecycleCallback : Java.Lang.Object, Application.IActivityLifecycleCallbacks, IMvxSetupMonitor, IMvxAndroidCurrentTopActivity
{
protected IMvxAndroidActivityLifetimeListener listener;
protected readonly List<string> startedActivityNames = new List<string>();

protected int startedActivities;
protected Activity startupActivity;
protected bool isStartupActivityDisplayed;
protected readonly List<Activity> resumedActivities = new List<Activity>(); //all activities after resumed and until paused

protected Bundle _bundle;
protected MvxAndroidSetupSingleton setupSingleton;

protected Dictionary<string,string> pushedData; //Non null when the app is launched from a push notification

public Activity Activity => resumedActivities.LastOrDefault();

public MvxStartupLifecycleCallback()
{
}

protected MvxStartupLifecycleCallback(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer)
{
}

public virtual void OnActivityCreated(Activity activity, Bundle bundle)
{
Console.WriteLine($"(SLC) OnActivityCreated {activity.GetType().Name}. Is StartupActivity:{(activity is IStartupActivity ? "yes" : "no")}");

if (activity is IStartupActivity)
{
if (Activity == null)
{
_bundle = bundle;

//Non null when the app is launched from a push notification
var extras = activity.Intent?.Extras;
if (extras != null)
{
var customData = new Dictionary<string, string>();
foreach (var key in extras.KeySet())
{
var o = extras.Get(key);
if (o != null)
customData.Add(key, o.ToString());
}

pushedData = customData;
}
}
else
{
Console.WriteLine($"(SLC) OnActivityCreated IStartupActivity!=null :{Activity.GetType().Name}");
}
}

if (activity is IMvxAndroidView mvxAndroidActivity)
mvxAndroidActivity.OnViewCreate(bundle);

if (activity is IMvxView mvxActivity)
mvxActivity.ViewModel?.ViewCreated();

listener?.OnCreate(activity, bundle);
}

public virtual void OnActivityDestroyed(Activity activity)
{
//Console.WriteLine($"(SLC) OnActivityDestroyed {activity.GetType().Name} istaskRoot:{activity.IsTaskRoot} isShutdownActivity:{activity is IShutdownActivity} isFinishing:{activity.IsFinishing}");
listener?.OnDestroy(activity);

if (activity is IMvxView mvxActivity)
DestroyActivity(mvxActivity);

if (activity.IsTaskRoot && activity is IShutdownActivity && Mvx.IoCProvider!=null && activity.IsFinishing)
{
Console.WriteLine("(SLC) Application deactivated");
SetState(ApplicationState.Deactivation);
if(setupSingleton != null)
Console.WriteLine("(SLC) setupSingleton monitor cancelled");
var startup = Mvx.IoCProvider.Resolve<IMvxAppStart>();
startup.ResetStart();
setupSingleton?.CancelMonitor(this);
setupSingleton?.Dispose();
setupSingleton = null;
}
}

public virtual void OnActivityPaused(Activity activity)
{
//Console.WriteLine($"(SLC) OnActivityPaused {activity.GetType().Name}");
resumedActivities.Remove(activity);
listener?.OnPause(activity);

if (activity is IMvxView mvxActivity)
mvxActivity.ViewModel?.ViewDisappearing();
}

public virtual void OnActivityResumed(Activity activity)
{
resumedActivities.Add(activity);

//Console.WriteLine($"(SLC) OnActivityResumed {activity.GetType().Name} isStartupActivityDisplayed:{isStartupActivityDisplayed} isStartupActivity:{activity is IStartupActivity} startupActivity:{startupActivity?.GetType().Name ?? "-"} top:{Activity?.GetType().Name??"-"} startedActivities:{startedActivities} {startedActivityNames.Aggregate(new StringBuilder(), (sb,s) => sb.Append(s).Append(','))}");

//Note: on restart (tap on app icon), top.Activity = IStartupActivity and startupActivity = null
var isFirstStart = Activity == null;
var isOtherStart = Activity is IStartupActivity;
var isStartingWithAutoClosingActivity = isFirstStart || isOtherStart;
if (activity is IStartupActivity && isStartingWithAutoClosingActivity)
{
//Splash can be resumed twice, for example when appcenter distribute is embedded and the app is first run (as appcenter temporarily switches to the device's browser before the real main activity is displayed)
//Also, Splash can be recreated by tapping an Android notification while the app is in background. In this last case, startup.IsStarted will be true (and startedActivities will be greater than 1).
if (!isStartupActivityDisplayed)
{
isStartupActivityDisplayed = true;
startupActivity = activity;
setupSingleton = setupSingleton ?? MvxAndroidSetupSingleton.EnsureSingletonAvailable(Application.Context);
setupSingleton.InitializeAndMonitor(this); //Resume is too late here, the top activity monitor is not up, and will not detect that the current top activity is a splash screen. The fragment presenter will be unable to present the activity.
}
}
else if (isStartupActivityDisplayed)
{
//startup activity is always destroyed
isStartupActivityDisplayed = false;
startupActivity = null;
SetState(ApplicationState.Foreground);
}
else if (startedActivities == 1)
{
SetState(ApplicationState.Foreground);
}

if (activity is IMvxView mvxActivity)
mvxActivity.ViewModel?.ViewAppeared();

listener?.OnResume(activity);
}

public virtual void OnActivitySaveInstanceState(Activity activity, Bundle outState)
{
//Console.WriteLine($"(SLC) OnActivitySaveInstanceState {activity.GetType().Name}");
resumedActivities.Remove(activity);
listener?.OnSaveInstanceState(activity, outState);
}

/// <summary>
/// Android always starts new activity just before stopping previous one.
/// </summary>
public virtual void OnActivityStarted(Activity activity)
{
Console.WriteLine($"(SLC) OnActivityStarted {activity.GetType().Name}");
startedActivities++;
startedActivityNames.Add(activity.GetType().Name);

if (activity is IMvxView mvxActivity)
mvxActivity.ViewModel?.ViewAppearing();

listener?.OnStart(activity);
}

public virtual void OnActivityStopped(Activity activity)
{
Console.WriteLine($"(SLC) OnActivityStopped {activity.GetType().Name} iStartupActivity:{activity is IStartupActivity} isStartupActivityDisplayed:{isStartupActivityDisplayed} startedActivities:{startedActivities}");

if (startedActivities == 1 && (!(activity is IStartupActivity) || isStartupActivityDisplayed))
SetState(ApplicationState.Background);

if (startedActivities > 0)
{
startedActivities--;
startedActivityNames.RemoveAt(startedActivities);
}

if (activity is IMvxView mvxActivity)
mvxActivity.ViewModel?.ViewDisappeared();

listener?.OnStop(activity);
}

/// <summary>
/// Setup triggers this 'initialization completed' event
/// </summary>
public virtual Task InitializationComplete()
{
var hint = GetAppStartHint(_bundle);
_bundle = null;
return RunAppStartAsync(hint);
}

protected virtual object GetAppStartHint(Bundle bundle)
{
//if(bundle != null)
// Console.WriteLine($"(SLC) AppStartHint: {JsonConvert.SerializeObject(bundle)}");
return null;
}

protected virtual async Task RunAppStartAsync(object hint)
{
var ioc = Mvx.IoCProvider;
ioc.CallbackWhenRegistered<IMvxAndroidActivityLifetimeListener>(() => listener = Mvx.IoCProvider.GetSingleton<IMvxAndroidActivityLifetimeListener>());

var startup = ioc.Resolve<IMvxAppStart>();
//Console.WriteLine($"(SLC) RunAppStartAsync isStarted:{startup.IsStarted} isStartupActivityDisplayed:{isStartupActivityDisplayed} startupActivity:{startupActivity?.GetType().Name}");

//Non null when the app is launched from a push notification
if (pushedData != null)
OnPushedReceived(pushedData);

if (startup.IsStarted)
{
//App has been reactivated from background (via a notification or a tap on the app's icon)
Console.WriteLine("(SLC) Application activated");

//When app is reactivated, splash screen activity is displayed.
//Remove it before triggering Activation state change.
if (isStartupActivityDisplayed)
{
var activity = Activity;

//Fix a rare crash
var i = 10;
while (activity == null)
{
await Task.Delay(i++);
activity = Activity;
if (i == 100)
{
Console.WriteLine($"(SLC) Error: startup activity {activity.GetType().Name} can not be destroyed.");
return;
}
}

((IStartupActivity)activity).FinishActivity();
}

SetState(ApplicationState.Foreground);

return;
}

//App 1st start
Console.WriteLine("(SLC) Application (re) start");

await startup.StartAsync(hint);
}

protected virtual void OnPushedReceived(Dictionary<string, string> pushedData)
{
}

protected virtual void SetState(ApplicationState applicationState)
{
Console.WriteLine($"Application state: {applicationState}");
}

protected virtual void DestroyActivity(IMvxView mvxActivity)
{
//(mvxActivity.ViewModel as IBackViewModel)?.NonCancellableBackCommand.Execute(null);
mvxActivity.ViewModel?.ViewDestroy();
}
}
}

0 comments on commit de75513

Please sign in to comment.