package com.hannesdorfmann.mosby3;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Bundle;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.view.View;
import com.hannesdorfmann.mosby3.mvp.MvpPresenter;
import com.hannesdorfmann.mosby3.mvp.MvpView;
import java.util.Map;
import java.util.UUID;
/**
* A internal class responsible to save internal presenter instances during screen orientation
* changes and reattach the presenter afterwards.
*
* <p>
* The idea is that each MVP View (like a Activity, Fragment, ViewGroup) will get a unique view id.
* This view id is
* used to store the presenter and viewstate in it. After screen orientation changes we can reuse
* the presenter and viewstate by querying for the given view id (must be saved in view's state
* somehow).
* </p>
*
* @author Hannes Dorfmann
* @since 3.0
*/
final public class PresenterManager {
public static boolean DEBUG = false;
public static final String DEBUG_TAG = "PresenterManager";
final static String KEY_ACTIVITY_ID = "com.hannesdorfmann.mosby3.MosbyPresenterManagerActivityId";
private final static Map<Activity, String> activityIdMap = new ArrayMap<>();
private final static Map<String, ActivityScopedCache> activityScopedCacheMap = new ArrayMap<>();
static final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (savedInstanceState != null) {
String activityId = savedInstanceState.getString(KEY_ACTIVITY_ID);
if (activityId != null) {
// After a screen orientation change we map the newly created Activity to the same
// Activity ID as the previous activity has had (before screen orientation change)
activityIdMap.put(activity, activityId);
}
}
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
// Save the activityId into bundle so that the other
String activityId = activityIdMap.get(activity);
if (activityId != null) {
outState.putString(KEY_ACTIVITY_ID, activityId);
}
}
@Override public void onActivityStarted(Activity activity) {
}
@Override public void onActivityResumed(Activity activity) {
}
@Override public void onActivityPaused(Activity activity) {
}
@Override public void onActivityStopped(Activity activity) {
}
@Override public void onActivityDestroyed(Activity activity) {
if (!activity.isChangingConfigurations()) {
// Activity will be destroyed permanently, so reset the cache
String activityId = activityIdMap.get(activity);
if (activityId != null) {
ActivityScopedCache scopedCache = activityScopedCacheMap.get(activityId);
if (scopedCache != null) {
scopedCache.clear();
activityScopedCacheMap.remove(activityId);
}
// No Activity Scoped cache available, so unregister
if (activityScopedCacheMap.isEmpty()) {
// All Mosby related activities are destroyed, so we can remove the activity lifecylce listener
activity.getApplication()
.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
if (DEBUG) {
Log.d(DEBUG_TAG, "Unregistering ActivityLifecycleCallbacks");
}
}
}
}
activityIdMap.remove(activity);
}
};
private PresenterManager() {
throw new RuntimeException("Not instantiatable!");
}
/**
* Get an already existing {@link ActivityScopedCache} or creates a new one if not existing yet
*
* @param activity The Activitiy for which you want to get the activity scope for
* @return The {@link ActivityScopedCache} for the given Activity
*/
@NonNull @MainThread static ActivityScopedCache getOrCreateActivityScopedCache(
@NonNull Activity activity) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
String activityId = activityIdMap.get(activity);
if (activityId == null) {
// Activity not registered yet
activityId = UUID.randomUUID().toString();
activityIdMap.put(activity, activityId);
if (activityIdMap.size() == 1) {
// Added the an Activity for the first time so register Activity LifecycleListener
activity.getApplication().registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
if (DEBUG) {
Log.d(DEBUG_TAG, "Registering ActivityLifecycleCallbacks");
}
}
}
ActivityScopedCache activityScopedCache = activityScopedCacheMap.get(activityId);
if (activityScopedCache == null) {
activityScopedCache = new ActivityScopedCache();
activityScopedCacheMap.put(activityId, activityScopedCache);
}
return activityScopedCache;
}
/**
* Get the {@link ActivityScopedCache} for the given Activity or <code>null</code> if no {@link
* ActivityScopedCache} exists for the given Activity
*
* @param activity The activity
* @return The {@link ActivityScopedCache} or null
* @see #getOrCreateActivityScopedCache(Activity)
*/
@Nullable @MainThread static ActivityScopedCache getActivityScope(@NonNull Activity activity) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
String activityId = activityIdMap.get(activity);
if (activityId == null) {
return null;
}
return activityScopedCacheMap.get(activityId);
}
/**
* Get the presenter for the View with the given (Mosby - internal) view Id or <code>null</code>
* if no presenter for the given view (via view id) exists.
*
* @param activity The Activity (used for scoping)
* @param viewId The mosby internal View Id (unique among all {@link MvpView}
* @param <P> The Presenter type
* @return The Presenter or <code>null</code>
*/
@Nullable public static <P> P getPresenter(@NonNull Activity activity, @NonNull String viewId) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
if (viewId == null) {
throw new NullPointerException("View id is null");
}
ActivityScopedCache scopedCache = getActivityScope(activity);
return scopedCache == null ? null : (P) scopedCache.getPresenter(viewId);
}
/**
* Get the ViewState (see mosby viestate modlue) for the View with the given (Mosby - internal)
* view Id or <code>null</code>
* if no viewstate for the given view exists.
*
* @param activity The Activity (used for scoping)
* @param viewId The mosby internal View Id (unique among all {@link MvpView}
* @param <VS> The type of the ViewState type
* @return The Presenter or <code>null</code>
*/
@Nullable public static <VS> VS getViewState(@NonNull Activity activity, @NonNull String viewId) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
if (viewId == null) {
throw new NullPointerException("View id is null");
}
ActivityScopedCache scopedCache = getActivityScope(activity);
return scopedCache == null ? null : (VS) scopedCache.getViewState(viewId);
}
/**
* Get the Activity of a context. This is typically used to determine the hosting activity of a
* {@link View}
*
* @param context The context
* @return The Activity or throws an Exception if Activity couldnt be determined
*/
@NonNull public static Activity getActivity(@NonNull Context context) {
if (context == null) {
throw new NullPointerException("context == null");
}
if (context instanceof Activity) {
return (Activity) context;
}
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
throw new IllegalStateException("Could not find the surrounding Activity");
}
/**
* Clears the internal (static) state. Used for testing.
*/
static void reset() {
activityIdMap.clear();
for (ActivityScopedCache scopedCache : activityScopedCacheMap.values()) {
scopedCache.clear();
}
activityScopedCacheMap.clear();
}
/**
* Puts the presenter into the internal cache
*
* @param activity The parent activity
* @param viewId the view id (mosby internal)
* @param presenter the presenter
*/
public static void putPresenter(@NonNull Activity activity, @NonNull String viewId,
@NonNull MvpPresenter<? extends MvpView> presenter) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
ActivityScopedCache scopedCache = getOrCreateActivityScopedCache(activity);
scopedCache.putPresenter(viewId, presenter);
}
/**
* Puts the presenter into the internal cache
*
* @param activity The parent activity
* @param viewId the view id (mosby internal)
* @param viewState the presenter
*/
public static void putViewState(@NonNull Activity activity, @NonNull String viewId,
@NonNull Object viewState) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
ActivityScopedCache scopedCache = getOrCreateActivityScopedCache(activity);
scopedCache.putViewState(viewId, viewState);
}
/**
* Removes the Presenter (and ViewState) for the given View. Does nothing if no Presenter is
* stored internally with the given viewId
*
* @param activity The activity
* @param viewId The mosby internal view id
*/
public static void remove(@NonNull Activity activity, @NonNull String viewId) {
if (activity == null) {
throw new NullPointerException("Activity is null");
}
ActivityScopedCache activityScope = getActivityScope(activity);
if (activityScope != null) {
activityScope.remove(viewId);
}
}
}