1 /*
2  * Copyright (C) 2021 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.systemui.shared.plugins;
18 
19 import static junit.framework.Assert.assertEquals;
20 import static junit.framework.Assert.assertFalse;
21 import static junit.framework.Assert.assertNotNull;
22 import static junit.framework.Assert.assertNull;
23 import static junit.framework.Assert.assertTrue;
24 import static junit.framework.Assert.fail;
25 
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.pm.ApplicationInfo;
29 import android.util.Log;
30 
31 import androidx.test.filters.SmallTest;
32 import androidx.test.runner.AndroidJUnit4;
33 
34 import com.android.systemui.SysuiTestCase;
35 import com.android.systemui.plugins.Plugin;
36 import com.android.systemui.plugins.PluginLifecycleManager;
37 import com.android.systemui.plugins.PluginListener;
38 import com.android.systemui.plugins.annotations.ProvidesInterface;
39 import com.android.systemui.plugins.annotations.Requires;
40 
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 
45 import java.lang.ref.WeakReference;
46 import java.util.Collections;
47 import java.util.concurrent.Semaphore;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.atomic.AtomicInteger;
50 import java.util.concurrent.atomic.AtomicReference;
51 import java.util.function.Supplier;
52 
53 @SmallTest
54 @RunWith(AndroidJUnit4.class)
55 public class PluginInstanceTest extends SysuiTestCase {
56 
57     private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins";
58     private static final ComponentName TEST_PLUGIN_COMPONENT_NAME =
59             new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName());
60 
61     private FakeListener mPluginListener;
62     private VersionInfo mVersionInfo;
63     private VersionInfo.InvalidVersionException mVersionException;
64     private PluginInstance.VersionChecker mVersionChecker;
65 
66     private RefCounter mCounter;
67     private PluginInstance<TestPlugin> mPluginInstance;
68     private PluginInstance.Factory mPluginInstanceFactory;
69     private ApplicationInfo mAppInfo;
70 
71     // Because we're testing memory in this file, we must be careful not to assert the target
72     // objects, or capture them via mockito if we expect the garbage collector to later free them.
73     // Both JUnit and Mockito will save references and prevent these objects from being cleaned up.
74     private WeakReference<TestPluginImpl> mPlugin;
75     private WeakReference<Context> mPluginContext;
76 
77     @Before
setup()78     public void setup() throws Exception {
79         mCounter = new RefCounter();
80         mAppInfo = mContext.getApplicationInfo();
81         mAppInfo.packageName = TEST_PLUGIN_COMPONENT_NAME.getPackageName();
82         mPluginListener = new FakeListener();
83         mVersionInfo = new VersionInfo();
84         mVersionChecker = new PluginInstance.VersionChecker() {
85             @Override
86             public <T extends Plugin> VersionInfo checkVersion(
87                     Class<T> instanceClass,
88                     Class<T> pluginClass,
89                     Plugin plugin
90             ) {
91                 if (mVersionException != null) {
92                     throw mVersionException;
93                 }
94                 return mVersionInfo;
95             }
96         };
97 
98         mPluginInstanceFactory = new PluginInstance.Factory(
99                 this.getClass().getClassLoader(),
100                 new PluginInstance.InstanceFactory<TestPlugin>() {
101                     @Override
102                     TestPlugin create(Class cls) {
103                         TestPluginImpl plugin = new TestPluginImpl(mCounter);
104                         mPlugin = new WeakReference<>(plugin);
105                         return plugin;
106                     }
107                 },
108                 mVersionChecker,
109                 Collections.singletonList(PRIVILEGED_PACKAGE),
110                 false);
111 
112         mPluginInstance = mPluginInstanceFactory.create(
113                 mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME,
114                 TestPlugin.class, mPluginListener);
115         mPluginInstance.setLogFunc((tag, msg) -> Log.d((String) tag, (String) msg));
116         mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext());
117     }
118 
119     @Test
testCorrectVersion()120     public void testCorrectVersion() {
121         assertNotNull(mPluginInstance);
122     }
123 
124     @Test(expected = VersionInfo.InvalidVersionException.class)
testIncorrectVersion()125     public void testIncorrectVersion() throws Exception {
126         ComponentName wrongVersionTestPluginComponentName =
127                 new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
128 
129         mVersionException = new VersionInfo.InvalidVersionException("test", true);
130 
131         mPluginInstanceFactory.create(
132                 mContext, mAppInfo, wrongVersionTestPluginComponentName,
133                 TestPlugin.class, mPluginListener);
134         mPluginInstance.onCreate();
135     }
136 
137     @Test
testOnCreate()138     public void testOnCreate() {
139         mPluginInstance.onCreate();
140         assertEquals(1, mPluginListener.mAttachedCount);
141         assertEquals(1, mPluginListener.mLoadCount);
142         assertEquals(mPlugin.get(), mPluginInstance.getPlugin());
143         assertInstances(1, 1);
144     }
145 
146     @Test
testOnDestroy()147     public void testOnDestroy() {
148         mPluginInstance.onCreate();
149         mPluginInstance.onDestroy();
150         assertEquals(1, mPluginListener.mDetachedCount);
151         assertEquals(1, mPluginListener.mUnloadCount);
152         assertNull(mPluginInstance.getPlugin());
153         assertInstances(0, 0); // Destroyed but never created
154     }
155 
156     @Test
testOnUnloadAfterLoad()157     public void testOnUnloadAfterLoad() {
158         mPluginInstance.onCreate();
159         mPluginInstance.loadPlugin();
160         assertNotNull(mPluginInstance.getPlugin());
161         assertInstances(1, 1);
162 
163         mPluginInstance.unloadPlugin();
164         assertNull(mPluginInstance.getPlugin());
165         assertInstances(0, 0);
166     }
167 
168     @Test
testOnAttach_SkipLoad()169     public void testOnAttach_SkipLoad() {
170         mPluginListener.mOnAttach = () -> false;
171         mPluginInstance.onCreate();
172         assertEquals(1, mPluginListener.mAttachedCount);
173         assertEquals(0, mPluginListener.mLoadCount);
174         assertNull(mPluginInstance.getPlugin());
175         assertInstances(0, 0);
176     }
177 
178     @Test
testLoadUnloadSimultaneous_HoldsUnload()179     public void testLoadUnloadSimultaneous_HoldsUnload() throws Throwable {
180         final Semaphore loadLock = new Semaphore(1);
181         final Semaphore unloadLock = new Semaphore(1);
182 
183         mPluginListener.mOnAttach = () -> false;
184         mPluginListener.mOnLoad = () -> {
185             assertNotNull(mPluginInstance.getPlugin());
186 
187             // Allow the bg thread the opportunity to delete the plugin
188             loadLock.release();
189             Thread.yield();
190             boolean isLocked = getLock(unloadLock, 1000);
191 
192             // Ensure the bg thread failed to delete the plugin
193             assertNotNull(mPluginInstance.getPlugin());
194             // We expect that bgThread deadlocked holding the semaphore
195             assertFalse(isLocked);
196         };
197 
198         AtomicReference<Throwable> bgFailure = new AtomicReference<Throwable>(null);
199         Thread bgThread = new Thread(() -> {
200             assertTrue(getLock(unloadLock, 10));
201             assertTrue(getLock(loadLock, 10000)); // Wait for the foreground thread
202             assertNotNull(mPluginInstance.getPlugin());
203             // Attempt to delete the plugin, this should block until the load completes
204             mPluginInstance.unloadPlugin();
205             assertNull(mPluginInstance.getPlugin());
206             unloadLock.release();
207             loadLock.release();
208         });
209 
210         // This protects the test suite from crashing due to the uncaught exception.
211         bgThread.setUncaughtExceptionHandler((Thread t, Throwable ex) -> {
212             Log.e("PluginInstanceTest#testLoadUnloadSimultaneous_HoldsUnload",
213                     "Exception from BG Thread", ex);
214             bgFailure.set(ex);
215         });
216 
217         loadLock.acquire();
218         mPluginInstance.onCreate();
219 
220         assertNull(mPluginInstance.getPlugin());
221         bgThread.start();
222         mPluginInstance.loadPlugin();
223 
224         bgThread.join(5000);
225 
226         // Rethrow final background exception on test thread
227         Throwable bgEx = bgFailure.get();
228         if (bgEx != null) {
229             throw bgEx;
230         }
231 
232         assertNull(mPluginInstance.getPlugin());
233     }
234 
getLock(Semaphore lock, long millis)235     private boolean getLock(Semaphore lock, long millis) {
236         try {
237             return lock.tryAcquire(millis, TimeUnit.MILLISECONDS);
238         } catch (InterruptedException ex) {
239             Log.e("PluginInstanceTest#getLock",
240                     "Interrupted Exception getting lock", ex);
241             fail();
242             return false;
243         }
244     }
245 
246     // This target class doesn't matter, it just needs to have a Requires to hit the flow where
247     // the mock version info is called.
248     @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
249     public interface TestPlugin extends Plugin {
250         int VERSION = 1;
251         String ACTION = "testAction";
252     }
253 
assertInstances(int allocated, int created)254     private void assertInstances(int allocated, int created) {
255         // If there are more than the expected number of allocated instances, then we run the
256         // garbage collector to finalize and deallocate any outstanding non-referenced instances.
257         // Since the GC doesn't always appear to want to run completely when we ask, we do this up
258         // to 10 times before failing the test.
259         for (int i = 0; mCounter.getAllocatedInstances() > allocated && i < 10; i++) {
260             System.runFinalization();
261             System.gc();
262         }
263 
264         assertEquals(allocated, mCounter.getAllocatedInstances());
265         assertEquals(created, mCounter.getCreatedInstances());
266     }
267 
268     public static class RefCounter {
269         public final AtomicInteger mAllocatedInstances = new AtomicInteger();
270         public final AtomicInteger mCreatedInstances = new AtomicInteger();
271 
getAllocatedInstances()272         public int getAllocatedInstances() {
273             return mAllocatedInstances.get();
274         }
275 
getCreatedInstances()276         public int getCreatedInstances() {
277             return mCreatedInstances.get();
278         }
279     }
280 
281     @Requires(target = TestPlugin.class, version = TestPlugin.VERSION)
282     public static class TestPluginImpl implements TestPlugin {
283         public final RefCounter mCounter;
TestPluginImpl(RefCounter counter)284         public TestPluginImpl(RefCounter counter) {
285             mCounter = counter;
286             mCounter.mAllocatedInstances.getAndIncrement();
287         }
288 
289         @Override
finalize()290         public void finalize() {
291             mCounter.mAllocatedInstances.getAndDecrement();
292         }
293 
294         @Override
onCreate(Context sysuiContext, Context pluginContext)295         public void onCreate(Context sysuiContext, Context pluginContext) {
296             mCounter.mCreatedInstances.getAndIncrement();
297         }
298 
299         @Override
onDestroy()300         public void onDestroy() {
301             mCounter.mCreatedInstances.getAndDecrement();
302         }
303     }
304 
305     public class FakeListener implements PluginListener<TestPlugin> {
306         public Supplier<Boolean> mOnAttach = null;
307         public Runnable mOnDetach = null;
308         public Runnable mOnLoad = null;
309         public Runnable mOnUnload = null;
310         public int mAttachedCount = 0;
311         public int mDetachedCount = 0;
312         public int mLoadCount = 0;
313         public int mUnloadCount = 0;
314 
315         @Override
onPluginAttached(PluginLifecycleManager<TestPlugin> manager)316         public boolean onPluginAttached(PluginLifecycleManager<TestPlugin> manager) {
317             mAttachedCount++;
318             assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
319             return mOnAttach != null ? mOnAttach.get() : true;
320         }
321 
322         @Override
onPluginDetached(PluginLifecycleManager<TestPlugin> manager)323         public void onPluginDetached(PluginLifecycleManager<TestPlugin> manager) {
324             mDetachedCount++;
325             assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
326             if (mOnDetach != null) {
327                 mOnDetach.run();
328             }
329         }
330 
331         @Override
onPluginLoaded( TestPlugin plugin, Context pluginContext, PluginLifecycleManager<TestPlugin> manager )332         public void onPluginLoaded(
333                 TestPlugin plugin,
334                 Context pluginContext,
335                 PluginLifecycleManager<TestPlugin> manager
336         ) {
337             mLoadCount++;
338             TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get();
339             if (expectedPlugin != null) {
340                 assertEquals(expectedPlugin, plugin);
341             }
342             Context expectedContext = PluginInstanceTest.this.mPluginContext.get();
343             if (expectedContext != null) {
344                 assertEquals(expectedContext, pluginContext);
345             }
346             assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
347             if (mOnLoad != null) {
348                 mOnLoad.run();
349             }
350         }
351 
352         @Override
onPluginUnloaded( TestPlugin plugin, PluginLifecycleManager<TestPlugin> manager )353         public void onPluginUnloaded(
354                 TestPlugin plugin,
355                 PluginLifecycleManager<TestPlugin> manager
356         ) {
357             mUnloadCount++;
358             TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get();
359             if (expectedPlugin != null) {
360                 assertEquals(expectedPlugin, plugin);
361             }
362             assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
363             if (mOnUnload != null) {
364                 mOnUnload.run();
365             }
366         }
367     }
368 }
369