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 com.android.server.wm.utils.CommonUtils; 38 39 import org.junit.After; 40 import org.junit.Test; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 /** 46 * Tests for Activity leaks. 47 * 48 * Build/Install/Run: 49 * atest WmTests:ActivityLeakTests 50 */ 51 public class ActivityLeakTests { 52 53 private final Instrumentation mInstrumentation = getInstrumentation(); 54 private final Context mContext = mInstrumentation.getTargetContext(); 55 private final List<Activity> mStartedActivityList = new ArrayList<>(); 56 57 @After tearDown()58 public void tearDown() { 59 mInstrumentation.runOnMainSync(() -> { 60 // Reset strict mode. 61 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build()); 62 }); 63 for (Activity activity : mStartedActivityList) { 64 if (!activity.isDestroyed()) { 65 activity.finish(); 66 } 67 } 68 if (!mStartedActivityList.isEmpty()) { 69 CommonUtils.waitUntilActivityRemoved( 70 mStartedActivityList.get(mStartedActivityList.size() - 1)); 71 } 72 mStartedActivityList.clear(); 73 } 74 75 @Test testActivityLeak()76 public void testActivityLeak() { 77 final Bundle intentExtras = new Bundle(); 78 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true); 79 final DetectLeakActivity activity = (DetectLeakActivity) startActivity( 80 DetectLeakActivity.class, 0 /* flags */, intentExtras); 81 mStartedActivityList.add(activity); 82 83 activity.finish(); 84 85 assertFalse("Leak found on activity", activity.isLeakedAfterDestroy()); 86 } 87 88 @Test testActivityLeakForTwoInstances()89 public void testActivityLeakForTwoInstances() { 90 final Bundle intentExtras = new Bundle(); 91 92 // Launch an activity, then enable strict mode 93 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true); 94 final DetectLeakActivity activity1 = (DetectLeakActivity) startActivity( 95 DetectLeakActivity.class, 0 /* flags */, intentExtras); 96 mStartedActivityList.add(activity1); 97 98 // Launch second activity instance. 99 intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, false); 100 final DetectLeakActivity activity2 = (DetectLeakActivity) startActivity( 101 DetectLeakActivity.class, 102 FLAG_ACTIVITY_MULTIPLE_TASK | FLAG_ACTIVITY_NEW_DOCUMENT, intentExtras); 103 mStartedActivityList.add(activity2); 104 105 // Destroy the activity 106 activity1.finish(); 107 assertFalse("Leak found on activity 1", activity1.isLeakedAfterDestroy()); 108 109 activity2.finish(); 110 assertFalse("Leak found on activity 2", activity2.isLeakedAfterDestroy()); 111 } 112 startActivity(Class<?> cls, int flags, Bundle extras)113 private Activity startActivity(Class<?> cls, int flags, Bundle extras) { 114 final Intent intent = new Intent(mContext, cls); 115 intent.addFlags(flags | FLAG_ACTIVITY_NEW_TASK); 116 if (extras != null) { 117 intent.putExtras(extras); 118 } 119 return mInstrumentation.startActivitySync(intent); 120 } 121 122 public static class DetectLeakActivity extends Activity { 123 124 private static final String TAG = "DetectLeakActivity"; 125 126 public static final String ENABLE_STRICT_MODE = "enable_strict_mode"; 127 128 private volatile boolean mWasDestroyed; 129 private volatile boolean mIsLeaked; 130 131 @Override onCreate(Bundle savedInstanceState)132 protected void onCreate(Bundle savedInstanceState) { 133 super.onCreate(savedInstanceState); 134 if (getIntent().getBooleanExtra(ENABLE_STRICT_MODE, false)) { 135 enableStrictMode(); 136 } 137 } 138 139 @Override onDestroy()140 protected void onDestroy() { 141 super.onDestroy(); 142 getWindow().getDecorView().post(() -> { 143 synchronized (this) { 144 mWasDestroyed = true; 145 notifyAll(); 146 } 147 }); 148 } 149 isLeakedAfterDestroy()150 public boolean isLeakedAfterDestroy() { 151 synchronized (this) { 152 while (!mWasDestroyed && !mIsLeaked) { 153 try { 154 wait(5000 /* timeoutMs */); 155 } catch (InterruptedException ignored) { 156 } 157 } 158 } 159 return mIsLeaked; 160 } 161 enableStrictMode()162 private void enableStrictMode() { 163 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 164 .detectActivityLeaks() 165 .penaltyLog() 166 .penaltyListener(Runnable::run, violation -> { 167 if (!(violation instanceof InstanceCountViolation)) { 168 return; 169 } 170 synchronized (this) { 171 mIsLeaked = true; 172 notifyAll(); 173 } 174 Log.w(TAG, violation.toString() + ", " + dumpHprofData()); 175 }) 176 .build()); 177 } 178 dumpHprofData()179 private String dumpHprofData() { 180 try { 181 final String fileName = getDataDir().getPath() + "/ActivityLeakHeapDump.hprof"; 182 Debug.dumpHprofData(fileName); 183 return "memory dump filename: " + fileName; 184 } catch (Throwable e) { 185 Log.e(TAG, "dumpHprofData failed", e); 186 return "failed to save memory dump"; 187 } 188 } 189 } 190 } 191