package frenchtoast; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.ContextWrapper; import android.os.Bundle; import android.support.annotation.LayoutRes; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import static android.widget.Toast.LENGTH_SHORT; import static frenchtoast.ToastInternals.MAIN_HANDLER; import static frenchtoast.ToastInternals.assertMainThread; import static frenchtoast.ToastInternals.checkNotNull; public final class FrenchToast { private static QueueHolder queueHolder; @MainThread public static void install(@NonNull Application application) { assertMainThread(); checkNotNull(application, "application"); if (queueHolder != null) { throw new IllegalStateException("Already installed."); } queueHolder = new QueueHolder(); application.registerActivityLifecycleCallbacks(queueHolder); } @MainThread public static SmartToaster with(@NonNull Context context) { assertMainThread(); checkNotNull(context, "context"); Activity activity = unwrapActivity(context); return new ActivityToaster(activity); } private static Activity unwrapActivity(Context context) { Context appContext = context.getApplicationContext(); Context unwrapped = context; for (; ; ) { if (unwrapped instanceof Activity) { return (Activity) unwrapped; } if (unwrapped == null || unwrapped == appContext || !(unwrapped instanceof ContextWrapper)) { throw new IllegalArgumentException( "Could not find Activity in the chain of wrapped contexts from " + context); } Context baseContext = ((ContextWrapper) unwrapped).getBaseContext(); if (baseContext == unwrapped) { throw new IllegalArgumentException( "Could not find Activity in the chain of wrapped contexts from " + context); } unwrapped = baseContext; } } static final class Holder { boolean paused; LifecycleToastQueue queueOrNull; String savedUniqueId; } static final class ActivityToaster implements SmartToaster { /* * Constants based on https://github.com/android/platform_frameworks_base/blob/master/services/ * core/java/com/android/server/notification/NotificationManagerService.java#L140 */ private static final int ANDROID_LONG_DELAY_MS = 3_500; private static final int ANDROID_SHORT_DELAY_MS = 2_000; private static final int IGNORED = LENGTH_SHORT; private final Activity activity; private long durationMs = ANDROID_LONG_DELAY_MS; ActivityToaster(Activity activity) { this.activity = activity; } @Override @MainThread public Toaster shortLength() { assertMainThread(); durationMs = ANDROID_SHORT_DELAY_MS; return this; } @Override @MainThread public Toaster longLength() { assertMainThread(); durationMs = ANDROID_LONG_DELAY_MS; return this; } @Override @MainThread public Toaster length(long duration, TimeUnit timeUnit) { assertMainThread(); durationMs = timeUnit.toMillis(duration); return this; } @Override @MainThread public void clear() { assertMainThread(); queueHolder.clear(activity); } @Override public Toasted showText(CharSequence text) { @SuppressLint("ShowToast") Toast toast = Toast.makeText(activity.getApplicationContext(), text, IGNORED); return showDipped(toast); } @Override public Toasted showText(@StringRes int stringResId) { @SuppressLint("ShowToast") Toast toast = Toast.makeText(activity.getApplicationContext(), stringResId, IGNORED); return showDipped(toast); } @Override public Toasted showLayout(@LayoutRes int layoutResId) { Context context = activity.getApplicationContext(); View view = LayoutInflater.from(context).inflate(layoutResId, null); Toast toast = new Toast(context); toast.setView(view); return showDipped(toast); } @Override @MainThread public Toasted showDipped(Toast toast) { assertMainThread(); Mixture mixture = Mixture.dip(toast); ToastQueue queue = queueHolder.getOrCreateActivityToastQueue(activity); queue.enqueue(mixture, durationMs); return new Toasted(queue, mixture); } } static final class QueueHolder extends ActivityLifecycleCallbacksAdapter { private static final String FRENCH_TOAST_ACTIVITY_UNIQUE_ID = "FRENCH_TOAST_ACTIVITY_UNIQUE_ID"; final Map<Activity, Holder> createdActivities = new LinkedHashMap<>(); final Map<String, LifecycleToastQueue> retainedQueues = new LinkedHashMap<>(); final Runnable clearRetainedQueues = new Runnable() { @Override public void run() { clearRetainedQueues(); } }; @Override public void onActivityPaused(Activity activity) { Holder holder = createdActivities.get(activity); holder.paused = true; if (holder.queueOrNull != null) { holder.queueOrNull.pause(); } } @Override public void onActivityResumed(Activity activity) { Holder holder = createdActivities.get(activity); holder.paused = false; if (holder.queueOrNull != null) { holder.queueOrNull.resume(); } holder.savedUniqueId = null; } @Override public void onActivityDestroyed(Activity activity) { Holder holder = createdActivities.remove(activity); if (holder.queueOrNull == null) { return; } if (activity.isChangingConfigurations() && holder.savedUniqueId != null) { retainedQueues.put(holder.savedUniqueId, holder.queueOrNull); // onCreate() is always called from the same message as the previous onDestroy(). MAIN_HANDLER.post(clearRetainedQueues); } else { holder.queueOrNull.clear(); } } private void clearRetainedQueues() { for (LifecycleToastQueue queue : retainedQueues.values()) { queue.clear(); } retainedQueues.clear(); } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { Holder holder = createdActivities.get(activity); String activityUniqueId = UUID.randomUUID().toString(); outState.putString(FRENCH_TOAST_ACTIVITY_UNIQUE_ID, activityUniqueId); holder.savedUniqueId = activityUniqueId; } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { Holder holder = new Holder(); createdActivities.put(activity, holder); if (!retainedQueues.isEmpty()) { String uniqueId = savedInstanceState.getString(FRENCH_TOAST_ACTIVITY_UNIQUE_ID); if (uniqueId != null) { holder.queueOrNull = retainedQueues.remove(uniqueId); } } } void clear(Activity activity) { Holder holder = getHolderOrThrow(activity); if (holder.queueOrNull != null) { holder.queueOrNull.clear(); } } ToastQueue getOrCreateActivityToastQueue(Activity activity) { Holder holder = getHolderOrThrow(activity); if (holder.queueOrNull == null) { LifecycleToastQueue toastQueue = new LifecycleToastQueue(); if (holder.paused) { toastQueue.pause(); } holder.queueOrNull = toastQueue; } return holder.queueOrNull; } private Holder getHolderOrThrow(Activity activity) { Holder holder = createdActivities.get(activity); if (holder == null) { throw new NullPointerException("Unknown activity " + activity + ", make sure it's not destroyed " + " and that you did not forget to call ActivityToasts.install() " + "from Application.onCreate()"); } return holder; } } private FrenchToast() { throw new AssertionError(); } }