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