blob: e26b4a1bc0cae01da7dfc5798a4b3cf3e379a0df [file] [log] [blame]
/*
* Copyright (C) 2017 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.ir.server;
import static android.app.ActivityManager.ProcessErrorStateInfo.NO_ERROR;
import static com.android.tools.ir.server.Logging.LOG_TAG;
import android.app.Activity;
import android.app.ActivityManager;
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.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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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 {
/** 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);
}
});
}
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.)
*/
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);
}
}
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);
}
}
}
});
}
@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
@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);
// check app hasn't crashed, if it has, return empty list of activities.
if (hasAppCrashed(context, activityThreadClass, activityThread)) {
return new ArrayList<Activity>();
}
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 activityClientRecord : c) {
Class activityClientRecordClass = activityClientRecord.getClass();
if (foregroundOnly) {
Field pausedField = activityClientRecordClass.getDeclaredField("paused");
pausedField.setAccessible(true);
if (pausedField.getBoolean(activityClientRecord)) {
continue;
}
}
Field activityField = activityClientRecordClass.getDeclaredField("activity");
activityField.setAccessible(true);
Activity activity = (Activity) activityField.get(activityClientRecord);
if (activity != null) {
list.add(activity);
}
}
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.WARN)) {
Log.w(LOG_TAG, "Error retrieving activities", e);
}
}
return list;
}
/**
* Checks if the application has crashed by comparing the package name against the list of
* processes in error state.
*/
private static boolean hasAppCrashed(
@Nullable Context context,
@NonNull Class activityThreadClass,
@Nullable Object activityThread)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
if (context == null || activityThread == null) {
return false;
}
String currentPackageName = getPackageName(activityThreadClass, activityThread);
ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.ProcessErrorStateInfo> processesInErrorState =
manager.getProcessesInErrorState();
if (processesInErrorState != null) { // returns null if no process in error state
for (ActivityManager.ProcessErrorStateInfo info : processesInErrorState) {
if (info.processName.equals(currentPackageName) && info.condition != NO_ERROR) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "App Thread has crashed, return empty activity list.");
}
return true;
}
}
}
return false;
}
// Use reflection to determine the package name from activity thread.
private static String getPackageName(
@NonNull Class activityThreadClass, @Nullable Object activityThread)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method currentPackageNameMethod =
activityThreadClass.getDeclaredMethod("currentPackageName");
return (String) currentPackageNameMethod.invoke(activityThread);
}
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 &amp; 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);
}
}