|
1 | 1 | package ly.count.android.sdk; |
2 | 2 |
|
| 3 | +import android.app.Activity; |
3 | 4 | import androidx.test.ext.junit.runners.AndroidJUnit4; |
4 | 5 | import java.util.ArrayList; |
5 | 6 | import java.util.List; |
|
11 | 12 | import org.junit.Test; |
12 | 13 | import org.junit.runner.RunWith; |
13 | 14 |
|
| 15 | +import static org.mockito.Mockito.mock; |
| 16 | + |
14 | 17 | @RunWith(AndroidJUnit4.class) |
15 | 18 | public class ModuleContentTests { |
16 | 19 |
|
@@ -68,6 +71,12 @@ private void setIsCurrentlyInContentZone(ModuleContent module, boolean value) th |
68 | 71 | field.set(module, value); |
69 | 72 | } |
70 | 73 |
|
| 74 | + private Activity getCurrentActivity(ModuleContent module) throws Exception { |
| 75 | + java.lang.reflect.Field field = ModuleContent.class.getDeclaredField("currentActivity"); |
| 76 | + field.setAccessible(true); |
| 77 | + return (Activity) field.get(module); |
| 78 | + } |
| 79 | + |
71 | 80 | // ======== previewContent public API tests ======== |
72 | 81 |
|
73 | 82 | /** |
@@ -158,4 +167,92 @@ public void validateResponse() throws JSONException { |
158 | 167 | valid.put("html", "<html></html>"); |
159 | 168 | Assert.assertTrue(mc.validateResponse(valid)); |
160 | 169 | } |
| 170 | + |
| 171 | + // ======== Activity reference / leak prevention tests (issue #556) ======== |
| 172 | + |
| 173 | + /** |
| 174 | + * onActivityDestroyed must null out currentActivity when the destroyed activity |
| 175 | + * is the one currently tracked. This is the core leak fix. |
| 176 | + */ |
| 177 | + @Test |
| 178 | + public void onActivityDestroyed_clearsCurrentActivity_whenIdentityMatches() throws Exception { |
| 179 | + Countly countly = initWithConsent(true); |
| 180 | + ModuleContent mc = countly.moduleContent; |
| 181 | + |
| 182 | + Activity act = mock(Activity.class); |
| 183 | + mc.onActivityStarted(act, 1); |
| 184 | + Assert.assertSame(act, getCurrentActivity(mc)); |
| 185 | + |
| 186 | + mc.onActivityDestroyed(act); |
| 187 | + Assert.assertNull(getCurrentActivity(mc)); |
| 188 | + } |
| 189 | + |
| 190 | + /** |
| 191 | + * Destroying an activity other than the currently tracked one must NOT clear the field. |
| 192 | + * This protects against losing the active activity reference when an old, already-replaced |
| 193 | + * activity is finally destroyed. |
| 194 | + */ |
| 195 | + @Test |
| 196 | + public void onActivityDestroyed_doesNotClear_whenDifferentActivity() throws Exception { |
| 197 | + Countly countly = initWithConsent(true); |
| 198 | + ModuleContent mc = countly.moduleContent; |
| 199 | + |
| 200 | + Activity tracked = mock(Activity.class); |
| 201 | + Activity unrelated = mock(Activity.class); |
| 202 | + mc.onActivityStarted(tracked, 1); |
| 203 | + |
| 204 | + mc.onActivityDestroyed(unrelated); |
| 205 | + Assert.assertSame(tracked, getCurrentActivity(mc)); |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Rotation race regression: when onActivityStarted for the new activity fires before |
| 210 | + * onActivityDestroyed for the old one, destroying the old activity must not wipe out |
| 211 | + * the new tracked activity. |
| 212 | + */ |
| 213 | + @Test |
| 214 | + public void onActivityDestroyed_doesNotClearNewerActivity_afterRotationRace() throws Exception { |
| 215 | + Countly countly = initWithConsent(true); |
| 216 | + ModuleContent mc = countly.moduleContent; |
| 217 | + |
| 218 | + Activity oldAct = mock(Activity.class); |
| 219 | + Activity newAct = mock(Activity.class); |
| 220 | + |
| 221 | + mc.onActivityStarted(oldAct, 1); |
| 222 | + mc.onActivityStarted(newAct, 2); |
| 223 | + Assert.assertSame(newAct, getCurrentActivity(mc)); |
| 224 | + |
| 225 | + // Old activity is finally destroyed after the new one has already taken over. |
| 226 | + mc.onActivityDestroyed(oldAct); |
| 227 | + Assert.assertSame(newAct, getCurrentActivity(mc)); |
| 228 | + } |
| 229 | + |
| 230 | + /** |
| 231 | + * onActivityDestroyed must not throw when no activity has been tracked yet. |
| 232 | + */ |
| 233 | + @Test |
| 234 | + public void onActivityDestroyed_isSafe_whenNoActivityTracked() throws Exception { |
| 235 | + Countly countly = initWithConsent(true); |
| 236 | + ModuleContent mc = countly.moduleContent; |
| 237 | + |
| 238 | + Activity stray = mock(Activity.class); |
| 239 | + mc.onActivityDestroyed(stray); |
| 240 | + Assert.assertNull(getCurrentActivity(mc)); |
| 241 | + } |
| 242 | + |
| 243 | + /** |
| 244 | + * The seeded activity path (onInitialActivitySeeded) must also be cleared on destroy. |
| 245 | + */ |
| 246 | + @Test |
| 247 | + public void onActivityDestroyed_clearsSeededActivity() throws Exception { |
| 248 | + Countly countly = initWithConsent(true); |
| 249 | + ModuleContent mc = countly.moduleContent; |
| 250 | + |
| 251 | + Activity seeded = mock(Activity.class); |
| 252 | + mc.onInitialActivitySeeded(seeded); |
| 253 | + Assert.assertSame(seeded, getCurrentActivity(mc)); |
| 254 | + |
| 255 | + mc.onActivityDestroyed(seeded); |
| 256 | + Assert.assertNull(getCurrentActivity(mc)); |
| 257 | + } |
161 | 258 | } |
0 commit comments