/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.antfortune.freeline.util; import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.ArrayMap; import android.util.Log; import android.widget.Toast; import com.antfortune.freeline.resources.MonkeyPatcher; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Handler capable of restarting parts of the application in order for changes to become * apparent to the user: * <ul> * <li> Apply a tiny change immediately (possible if we can detect that the change * is only used in a limited context (such as in a layout) and we can directly * poke the view hierarchy and schedule a paint * <li> Apply a change to the current activity. We can restart just the activity * while the app continues running. * <li> Restart the app with state persistence (simulates what happens when a user * puts an app in the background, then it gets killed by the memory monitor, * and then restored when the user brings it back * <li> Restart the app completely. * </ul> */ public class Restarter { private static final String LOG_TAG = "Freeline.Restarter"; /** Restart an activity. Should preserve as much state as possible. */ public static void restartActivityOnUiThread(final Activity activity) { activity.runOnUiThread(new Runnable() { @Override public void run() { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "Resources updated: notify activities"); } updateActivity(activity); } }); } private static void restartActivity(Activity activity) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "About to restart " + activity.getClass().getSimpleName()); } // You can't restart activities that have parents: find the top-most activity while (activity.getParent() != null) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, activity.getClass().getSimpleName() + " is not a top level activity; restarting " + activity.getParent().getClass().getSimpleName() + " instead"); } activity = activity.getParent(); } // Directly supported by the framework! activity.recreate(); } /** * Attempt to restart the app. Ideally this should also try to preserve as much state as * possible: * <ul> * <li>The current activity</li> * <li>If possible, state in the current activity, and</li> * <li>The activity stack</li> * </ul> * * This may require some framework support. Apparently it may already be possible * (Dianne says to put the app in the background, kill it then restart it; need to * figure out how to do this.) */ public static void restartApp(Context appContext, Collection<Activity> knownActivities, boolean toast) { if (!knownActivities.isEmpty()) { // Can't live patch resources; instead, try to restart the current activity Activity foreground = getForegroundActivity(appContext); if (foreground != null) { // http://stackoverflow.com/questions/6609414/howto-programatically-restart-android-app //noinspection UnnecessaryLocalVariable if (toast) { showToast(foreground, "Restarting app to apply incompatible changes"); } if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "RESTARTING APP"); } @SuppressWarnings("UnnecessaryLocalVariable") // fore code clarify Context context = foreground; Intent intent = new Intent(context, foreground.getClass()); int intentId = 0; PendingIntent pendingIntent = PendingIntent.getActivity(context, intentId, intent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent); if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "Scheduling activity " + foreground + " to start after exiting process"); } } else { showToast(knownActivities.iterator().next(), "Unable to restart app"); if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "Couldn't find any foreground activities to restart " + "for resource refresh"); } } System.exit(0); } } static void showToast(final Activity activity, final String text) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "About to show toast for activity " + activity + ": " + text); } activity.runOnUiThread(new Runnable() { @Override public void run() { try { Context context = activity.getApplicationContext(); if (context instanceof ContextWrapper) { Context base = ((ContextWrapper) context).getBaseContext(); if (base == null) { if (Log.isLoggable(LOG_TAG, Log.WARN)) { Log.w(LOG_TAG, "Couldn't show toast: no base context"); } return; } } // For longer messages, leave the message up longer int duration = Toast.LENGTH_SHORT; if (text.length() >= 60 || text.indexOf('\n') != -1) { duration = Toast.LENGTH_LONG; } // Avoid crashing when not available, e.g. // java.lang.RuntimeException: Can't create handler inside thread that has // not called Looper.prepare() Toast.makeText(activity, text, duration).show(); } catch (Throwable e) { if (Log.isLoggable(LOG_TAG, Log.WARN)) { Log.w(LOG_TAG, "Couldn't show toast", e); } } } }); } public static Activity getForegroundActivity(Context context) { List<Activity> list = getActivities(context, true); return list.isEmpty() ? null : list.get(0); } // http://stackoverflow.com/questions/11411395/how-to-get-current-foreground-activity-context-in-android public static List<Activity> getActivities(Context context, boolean foregroundOnly) { List<Activity> list = new ArrayList<Activity>(); try { Class activityThreadClass = Class.forName("android.app.ActivityThread"); Object activityThread = MonkeyPatcher.getActivityThread(context, activityThreadClass); Field activitiesField = activityThreadClass.getDeclaredField("mActivities"); activitiesField.setAccessible(true); // TODO: On older platforms, cast this to a HashMap Collection c; Object collection = activitiesField.get(activityThread); if (collection instanceof HashMap) { // Older platforms Map activities = (HashMap) collection; c = activities.values(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && collection instanceof ArrayMap) { ArrayMap activities = (ArrayMap) collection; c = activities.values(); } else { return list; } for (Object activityRecord : c) { Class activityRecordClass = activityRecord.getClass(); if (foregroundOnly) { Field pausedField = activityRecordClass.getDeclaredField("paused"); pausedField.setAccessible(true); if (pausedField.getBoolean(activityRecord)) { continue; } } Field activityField = activityRecordClass.getDeclaredField("activity"); activityField.setAccessible(true); Activity activity = (Activity) activityField.get(activityRecord); if (activity != null) { list.add(activity); } } } catch (Throwable ignore) { } return list; } private static void updateActivity(Activity activity) { // This method can be called for activities that are not in the foreground, as long // as some of its resources have been updated. Therefore we'll need to make sure // that this activity is in the foreground, and if not do nothing. Ways to do // that are outlined here: // http://stackoverflow.com/questions/3667022/checking-if-an-android-application-is-running-in-the-background/5862048#5862048 // Try to force re-layout; there are many approaches; see // http://stackoverflow.com/questions/5991968/how-to-force-an-entire-layout-view-refresh // This doesn't seem to update themes properly -- may need to do recreate() instead! //getWindow().getDecorView().findViewById(android.R.id.content).invalidate(); // This is a bit of a sledgehammer. We should consider having an incremental updater, // similar to IntelliJ's Look & Feel updater which iterates to the view hierarchy // and tries to incrementally refresh the LAF delegates and force a repaint. // On the other hand, we may never be able to succeed with that, since there could be // UI elements on the screen cached from callbacks. I should probably *not* attempt // to try to poke the user's data models; recreating the current layout should be // enough (e.g. if a layout references @string/foo, we'll recreate those widgets // if (mLastContentView != -1) { // setContentView(mLastContentView); // } else { // recreate(); // } // -- nope, even that's iffy. I had code which *after* calling setContentView would // do some findViewById calls etc to reinitialize views. // // So what I should really try to do is have some knowledge about what changed, // and see if I can figure out that the change is minor (e.g. doesn't affect themes // or layout parameters etc), and if so, just try to poke the view hierarchy directly, // and if not, just recreate // if (changeManager.isSimpleDelta()) { // changeManager.applyDirectly(this); // } else { // Note: This doesn't handle manifest changes like changing the application title restartActivity(activity); } /** Show a toast when an activity becomes available (if possible). */ public static void showToastWhenPossible(Context context, String message) { Activity activity = Restarter.getForegroundActivity(context); if (activity != null) { Restarter.showToast(activity, message); } else { // Only try for about 10 seconds showToastWhenPossible(context, message, 10); } } private static void showToastWhenPossible( final Context context, final String message, final int remainingAttempts) { Looper mainLooper = Looper.getMainLooper(); Handler handler = new Handler(mainLooper); handler.postDelayed(new Runnable() { @Override public void run() { Activity activity = getForegroundActivity(context); if (activity != null) { showToast(activity, message); } else { if (remainingAttempts > 0) { showToastWhenPossible(context, message, remainingAttempts - 1); } } } }, 1000); } }