/* * 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.android.tools.fd.runtime; 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.android.annotations.NonNull; import com.android.annotations.Nullable; 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; import static com.android.tools.fd.runtime.BootstrapApplication.LOG_TAG; /** * 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> * * 1. restartActivityOnUiThread:只在 UiThread 线程执行 updateActivity(...) * 2. restartActivity:重启 Activity * - 2.1 拿到该 Activity 的最顶层 Parent Activity * - 2.2 然后用 最顶层 Parent Activity 执行 recreate 方法 * 3. restartApp:重启 App * - 3.1 判断 activities 是否没有内容 * - - 3.1.1 没有的话,这个方法就不做任何事情 * - - 3.1.2 有的话,继续 * - 3.2 获取前台 Activity * - - 3.2.1 前台 Activity 为 null,那么就拿到 activities 的第一个 Activity 打 Toast,然后直接关闭 App( 杀死进程 ) * - - 3.2.2 前台 Activity 为 存在,那么就拿 前台 Activity 打 Toast,然后继续 * - 3.3 定制了一个 PendingIntent 是为了在未来打开这个 前台 Activity * - 3.4 获取 AlarmManager,设置定时任务,再未来的 100ms 后,通过 PendingIntent 打开这个 前台 Activity * - 3.5 杀死进程,等待 3.4 的定时任务执行,并打开 前台 Activity,实现重启 App 的效果 * 4. showToast:显示 toast * - 4.1 尝试获取 activity 的 base context * - - 4.1.1 拿不到的话,return * - 4.2 如果如果 Toast 的内容大于 60 或者有换行( \n ),那么持续时间长。否则,短 * - 4.3 调用 Toast.makeText(...).show() 显示 Toast * 5. getForegroundActivity:获取前台显示的 Activity,也就是获取全部没有 paused 的 Activity,然后从这个取第一个 * 6. getActivities:获取没有 paused 的 Activity * - 6.1 反射获取 ActivityThread 的 mActivities Field * - 6.2 获取 mActivities 的值,根据版本兼容: * - - 6.2.1 拿不到的话,return * - - 6.2.2 如果 > 4.4 && 是 ArrayMap 的话,转 * - - 6.2.3 都不是的话,会返回初始化好,没内容的 list * - 6.3 遍历 mActivities 值,拿到每一个 ActivityRecord * - - 6.3.1 判断是否是 foregroundOnly: * - - - 6.3.1.1 true 的话,过滤出 ActivityRecord 的 paused == true 的 ActivityRecord * - - - 6.3.1.2 false 的话,不走过滤逻辑 * - 6.4 然后反射 3. 下来的 ActivityRecord 的 activity Field * - 6.5 拿到 ActivityRecord 的 activity Field 的值,添加到 list 里 * 7. updateActivity:调用 restartActivity 重启 Activity * 8. showToastWhenPossible:如果可能的话,显示 Toast * - 8.1 获取前台 Activity * - 8.2.1 如果拿到了,就调用 Restarter.showToast(...) * - 8.2.2 如果没拿到,进入重试方法 showToastWhenPossible(...),根据重试次数,不断尝试显示 Toast * 9. showToastWhenPossible:重试显示 Toast 方法,根据重试次数,不断尝试显示 Toast * - 9.1 先实例化一个主线程 Handler,用于与主线程通信( 现在 Toast ) * - 9.2 然后希望在主线程执行的任务 Runnable 内,拿到获取前台显示 Activity * - - 9.2.1 如果此次拿到了,直接调用 showToast(...) 方法显示 Toast * - - 9.2.2 如果此次拿不到,那么递归到下次,继续尝试拿,一直递归到重试次数大于 0 为止 */ public class Restarter { /** * 只在 UiThread 线程执行 updateActivity(...) * * runOnUiThread 的原理很简单,就是判断是不是主线程,是的话,直接 run 这个 Runnable * 不是的话,调用 Activity 内置的主线程 mHandler ,给主线程的 MessageQueue 发 消息, * 回到主线程中处理该 Runnable */ /** Restart an activity. Should preserve as much state as possible. */ public static void restartActivityOnUiThread(@NonNull 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); } }); } /** * 重启 Activity * * 1. 拿到该 Activity 的最顶层 Parent Activity * 2. 然后用 最顶层 Parent Activity 执行 recreate 方法 * * @param activity activity */ private static void restartActivity(@NonNull 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.) * * 重启 App * * 1. 判断 activities 是否没有内容 * - 1.1 没有的话,这个方法就不做任何事情 * - 1.2 有的话,继续 * 2. 获取前台 Activity * - 2.1 前台 Activity 为 null,那么就拿到 activities 的第一个 Activity 打 Toast,然后直接关闭 App( 杀死进程 ) * - 2.2 前台 Activity 为 存在,那么就拿 前台 Activity 打 Toast,然后继续 * 3. 定制了一个 PendingIntent 是为了在未来打开这个 前台 Activity * 4. 获取 AlarmManager,设置定时任务,在未来的 100ms 后,通过 PendingIntent 打开这个 前台 Activity * 5. 杀死进程,等待 4. 的定时任务执行,并打开 前台 Activity,实现重启 App 的效果 */ public static void restartApp(@Nullable Context appContext, @NonNull 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); } } /** * 显示 toast * * 1. 尝试获取 activity 的 base context * - 1.1 拿不到的话,return * - 1.2 拿到的话,继续 * 2. 如果如果 Toast 的内容大于 60 或者有换行( \n ),那么持续时间长。否则,短 * 3. 调用 Toast.makeText(...).show() 显示 Toast * * @param activity activity * @param text text */ static void showToast(@NonNull final Activity activity, @NonNull 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); } } } }); } /** * 获取前台显示的 Activity * * 也就是获取全部没有 paused 的 Activity,然后从这个取第一个 * * @param context context * @return 前台 Activity */ @Nullable public static Activity getForegroundActivity(@Nullable 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 /** * 获取没有 paused 的 Activity * * 1. 反射获取 ActivityThread 的 mActivities Field * 2. 获取 mActivities 的值,根据版本兼容: * - 2.1 如果是 HashMap 的话,转 * - 2.2 如果 > 4.4 && 是 ArrayMap 的话,转 * - 2.3 都不是的话,会返回初始化好,没内容的 list * 3. 遍历 mActivities 值,拿到每一个 ActivityRecord * - 3.1 判断是否是 foregroundOnly: * - - true 的话,过滤出 ActivityRecord 的 paused == true 的 ActivityRecord * - - false 的话,不走过滤逻辑 * 4. 然后反射 3. 下来的 ActivityRecord 的 activity Field * 5. 拿到 ActivityRecord 的 activity Field 的值,添加到 list 里 * * @param context context * @param foregroundOnly foregroundOnly * @return activities */ @NonNull public static List<Activity> getActivities(@Nullable 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; } /** * 调用 restartActivity 重启 Activity * * @param activity activity */ private static void updateActivity(@NonNull 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). */ /** * 如果可能的话,显示 Toast * * 1. 获取前台 Activity * 2.1 如果拿到了,就调用 Restarter.showToast(...) * 2.2 如果没拿到,进入重试方法 showToastWhenPossible(...),根据重试次数,不断尝试显示 Toast * * @param context context * @param message toast 内容 */ public static void showToastWhenPossible(@Nullable Context context, @NonNull 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); } } /** * 重试显示 Toast 方法 * 根据重试次数,不断尝试显示 Toast * * 1. 先实例化一个主线程 Handler,用于与主线程通信( 现在 Toast ) * 2. 然后希望在主线程执行的任务 Runnable 内,拿到获取前台显示 Activity * - 2.1 如果此次拿到了,直接调用 showToast(...) 方法显示 Toast * - 2.2 如果此次拿不到,那么递归到下次,继续尝试拿,一直递归到重试次数大于 0 为止 * * @param context context * @param message message * @param remainingAttempts remainingAttempts */ private static void showToastWhenPossible( @Nullable final Context context, @NonNull 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); } }