1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wm; 18 19 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 20 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 22 23 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 24 25 import static org.junit.Assert.assertFalse; 26 27 import android.app.Activity; 28 import android.app.Instrumentation; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.os.Bundle; 32 import android.os.Debug; 33 import android.os.StrictMode; 34 import android.os.strictmode.InstanceCountViolation; 35 import android.util.Log; 36 37 import org.junit.After; 38 import org.junit.Test; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Tests for Activity leaks. 45 * 46 * Build/Install/Run: 47 * atest WmTests:ActivityLeakTests 48 */ 49 public class ActivityLeakTests { 50 51 private final Instrumentation mInstrumentation = getInstrumentation(); 52 private final Context mContext = mInstrumentation.getTargetContext(); 53 private final List<Activity> mStartedActivityList = new ArrayList<>(); 54 55 @After tearDown()56 public void tearDown() { 57 mInstrumentation.runOnMainSync(() -> { 58 // Reset strict mode. 59 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build()); 60 }); 61 for (Activity activity : mStartedActivityList) { 62 if (!activity.isDestroyed()) { 63 activity.finish(); 64 } 65 } 66 mStartedActivityList.clear(); 67 } 68 69 @Test testActivityLeak()70 public void testActivityLeak() { 71 final Bundle intentExtras = new Bundle(); 72 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true); 73 final DetectLeakActivity activity = (DetectLeakActivity) startActivity( 74 DetectLeakActivity.class, 0 /* flags */, intentExtras); 75 mStartedActivityList.add(activity); 76 77 activity.finish(); 78 79 assertFalse("Leak found on activity", activity.isLeakedAfterDestroy()); 80 } 81 82 @Test testActivityLeakForTwoInstances()83 public void testActivityLeakForTwoInstances() { 84 final Bundle intentExtras = new Bundle(); 85 86 // Launch an activity, then enable strict mode 87 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true); 88 final DetectLeakActivity activity1 = (DetectLeakActivity) startActivity( 89 DetectLeakActivity.class, 0 /* flags */, intentExtras); 90 mStartedActivityList.add(activity1); 91 92 // Launch second activity instance. 93 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, false); 94 final DetectLeakActivity activity2 = (DetectLeakActivity) startActivity( 95 DetectLeakActivity.class, 96 FLAG_ACTIVITY_MULTIPLE_TASK | FLAG_ACTIVITY_NEW_DOCUMENT, intentExtras); 97 mStartedActivityList.add(activity2); 98 99 // Destroy the activity 100 activity1.finish(); 101 assertFalse("Leak found on activity 1", activity1.isLeakedAfterDestroy()); 102 103 activity2.finish(); 104 assertFalse("Leak found on activity 2", activity2.isLeakedAfterDestroy()); 105 } 106 startActivity(Class<?> cls, int flags, Bundle extras)107 private Activity startActivity(Class<?> cls, int flags, Bundle extras) { 108 final Intent intent = new Intent(mContext, cls); 109 intent.addFlags(flags | FLAG_ACTIVITY_NEW_TASK); 110 if (extras != null) { 111 intent.putExtras(extras); 112 } 113 return mInstrumentation.startActivitySync(intent); 114 } 115 116 public static class DetectLeakActivity extends Activity { 117 118 private static final String TAG = "DetectLeakActivity"; 119 120 public static final String ENABLE_STRICT_MODE = "enable_strict_mode"; 121 122 private volatile boolean mWasDestroyed; 123 private volatile boolean mIsLeaked; 124 125 @Override onCreate(Bundle savedInstanceState)126 protected void onCreate(Bundle savedInstanceState) { 127 super.onCreate(savedInstanceState); 128 if (getIntent().getBooleanExtra(ENABLE_STRICT_MODE, false)) { 129 enableStrictMode(); 130 } 131 } 132 133 @Override onDestroy()134 protected void onDestroy() { 135 super.onDestroy(); 136 getWindow().getDecorView().post(() -> { 137 synchronized (this) { 138 mWasDestroyed = true; 139 notifyAll(); 140 } 141 }); 142 } 143 isLeakedAfterDestroy()144 public boolean isLeakedAfterDestroy() { 145 synchronized (this) { 146 while (!mWasDestroyed && !mIsLeaked) { 147 try { 148 wait(5000 /* timeoutMs */); 149 } catch (InterruptedException ignored) { 150 } 151 } 152 } 153 return mIsLeaked; 154 } 155 enableStrictMode()156 private void enableStrictMode() { 157 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 158 .detectActivityLeaks() 159 .penaltyLog() 160 .penaltyListener(Runnable::run, violation -> { 161 if (!(violation instanceof InstanceCountViolation)) { 162 return; 163 } 164 synchronized (this) { 165 mIsLeaked = true; 166 notifyAll(); 167 } 168 Log.w(TAG, violation.toString() + ", " + dumpHprofData()); 169 }) 170 .build()); 171 } 172 dumpHprofData()173 private String dumpHprofData() { 174 try { 175 final String fileName = getDataDir().getPath() + "/ActivityLeakHeapDump.hprof"; 176 Debug.dumpHprofData(fileName); 177 return "memory dump filename: " + fileName; 178 } catch (Throwable e) { 179 Log.e(TAG, "dumpHprofData failed", e); 180 return "failed to save memory dump"; 181 } 182 } 183 } 184 } 185