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