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.assertNotNull; 21 import static junit.framework.Assert.assertNull; 22 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.pm.ApplicationInfo; 26 import android.test.suitebuilder.annotation.SmallTest; 27 28 import androidx.test.runner.AndroidJUnit4; 29 30 import com.android.systemui.SysuiTestCase; 31 import com.android.systemui.plugins.Plugin; 32 import com.android.systemui.plugins.PluginLifecycleManager; 33 import com.android.systemui.plugins.PluginListener; 34 import com.android.systemui.plugins.annotations.ProvidesInterface; 35 import com.android.systemui.plugins.annotations.Requires; 36 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 41 import java.lang.ref.WeakReference; 42 import java.util.Collections; 43 import java.util.concurrent.atomic.AtomicInteger; 44 45 @SmallTest 46 @RunWith(AndroidJUnit4.class) 47 public class PluginInstanceTest extends SysuiTestCase { 48 49 private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins"; 50 private static final ComponentName TEST_PLUGIN_COMPONENT_NAME = 51 new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName()); 52 53 private FakeListener mPluginListener; 54 private VersionInfo mVersionInfo; 55 private VersionInfo.InvalidVersionException mVersionException; 56 private PluginInstance.VersionChecker mVersionChecker; 57 58 private RefCounter mCounter; 59 private PluginInstance<TestPlugin> mPluginInstance; 60 private PluginInstance.Factory mPluginInstanceFactory; 61 private ApplicationInfo mAppInfo; 62 63 // Because we're testing memory in this file, we must be careful not to assert the target 64 // objects, or capture them via mockito if we expect the garbage collector to later free them. 65 // Both JUnit and Mockito will save references and prevent these objects from being cleaned up. 66 private WeakReference<TestPluginImpl> mPlugin; 67 private WeakReference<Context> mPluginContext; 68 69 @Before setup()70 public void setup() throws Exception { 71 mCounter = new RefCounter(); 72 mAppInfo = mContext.getApplicationInfo(); 73 mAppInfo.packageName = TEST_PLUGIN_COMPONENT_NAME.getPackageName(); 74 mPluginListener = new FakeListener(); 75 mVersionInfo = new VersionInfo(); 76 mVersionChecker = new PluginInstance.VersionChecker() { 77 @Override 78 public <T extends Plugin> VersionInfo checkVersion( 79 Class<T> instanceClass, 80 Class<T> pluginClass, 81 Plugin plugin 82 ) { 83 if (mVersionException != null) { 84 throw mVersionException; 85 } 86 return mVersionInfo; 87 } 88 }; 89 90 mPluginInstanceFactory = new PluginInstance.Factory( 91 this.getClass().getClassLoader(), 92 new PluginInstance.InstanceFactory<TestPlugin>() { 93 @Override 94 TestPlugin create(Class cls) { 95 TestPluginImpl plugin = new TestPluginImpl(mCounter); 96 mPlugin = new WeakReference<>(plugin); 97 return plugin; 98 } 99 }, 100 mVersionChecker, 101 Collections.singletonList(PRIVILEGED_PACKAGE), 102 false); 103 104 mPluginInstance = mPluginInstanceFactory.create( 105 mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME, 106 TestPlugin.class, mPluginListener); 107 mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext()); 108 } 109 110 @Test testCorrectVersion()111 public void testCorrectVersion() { 112 assertNotNull(mPluginInstance); 113 } 114 115 @Test(expected = VersionInfo.InvalidVersionException.class) testIncorrectVersion()116 public void testIncorrectVersion() throws Exception { 117 ComponentName wrongVersionTestPluginComponentName = 118 new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName()); 119 120 mVersionException = new VersionInfo.InvalidVersionException("test", true); 121 122 mPluginInstanceFactory.create( 123 mContext, mAppInfo, wrongVersionTestPluginComponentName, 124 TestPlugin.class, mPluginListener); 125 mPluginInstance.onCreate(); 126 } 127 128 @Test testOnCreate()129 public void testOnCreate() { 130 mPluginInstance.onCreate(); 131 assertEquals(1, mPluginListener.mAttachedCount); 132 assertEquals(1, mPluginListener.mLoadCount); 133 assertEquals(mPlugin.get(), mPluginInstance.getPlugin()); 134 assertInstances(1, 1); 135 } 136 137 @Test testOnDestroy()138 public void testOnDestroy() { 139 mPluginInstance.onCreate(); 140 mPluginInstance.onDestroy(); 141 assertEquals(1, mPluginListener.mDetachedCount); 142 assertEquals(1, mPluginListener.mUnloadCount); 143 assertNull(mPluginInstance.getPlugin()); 144 assertInstances(0, 0); // Destroyed but never created 145 } 146 147 @Test testOnRepeatedlyLoadUnload_PluginFreed()148 public void testOnRepeatedlyLoadUnload_PluginFreed() { 149 mPluginInstance.onCreate(); 150 mPluginInstance.loadPlugin(); 151 assertInstances(1, 1); 152 153 mPluginInstance.unloadPlugin(); 154 assertNull(mPluginInstance.getPlugin()); 155 assertInstances(0, 0); 156 157 mPluginInstance.loadPlugin(); 158 assertInstances(1, 1); 159 160 mPluginInstance.unloadPlugin(); 161 mPluginInstance.onDestroy(); 162 assertNull(mPluginInstance.getPlugin()); 163 assertInstances(0, 0); 164 } 165 166 @Test testOnAttach_SkipLoad()167 public void testOnAttach_SkipLoad() { 168 mPluginListener.mAttachReturn = false; 169 mPluginInstance.onCreate(); 170 assertEquals(1, mPluginListener.mAttachedCount); 171 assertEquals(0, mPluginListener.mLoadCount); 172 assertEquals(null, mPluginInstance.getPlugin()); 173 assertInstances(0, 0); 174 } 175 176 // This target class doesn't matter, it just needs to have a Requires to hit the flow where 177 // the mock version info is called. 178 @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION) 179 public interface TestPlugin extends Plugin { 180 int VERSION = 1; 181 String ACTION = "testAction"; 182 } 183 assertInstances(Integer allocated, Integer created)184 public void assertInstances(Integer allocated, Integer created) { 185 // Run the garbage collector to finalize and deallocate outstanding 186 // instances. Since the GC doesn't always appear to want to run 187 // completely when we ask, we ask it 10 times in a short loop. 188 for (int i = 0; i < 10; i++) { 189 System.runFinalization(); 190 System.gc(); 191 } 192 193 mCounter.assertInstances(allocated, created); 194 } 195 196 public static class RefCounter { 197 public final AtomicInteger mAllocatedInstances = new AtomicInteger(); 198 public final AtomicInteger mCreatedInstances = new AtomicInteger(); 199 assertInstances(Integer allocated, Integer created)200 public void assertInstances(Integer allocated, Integer created) { 201 if (allocated != null) { 202 assertEquals(allocated.intValue(), mAllocatedInstances.get()); 203 } 204 if (created != null) { 205 assertEquals(created.intValue(), mCreatedInstances.get()); 206 } 207 } 208 } 209 210 @Requires(target = TestPlugin.class, version = TestPlugin.VERSION) 211 public static class TestPluginImpl implements TestPlugin { 212 public final RefCounter mCounter; TestPluginImpl(RefCounter counter)213 public TestPluginImpl(RefCounter counter) { 214 mCounter = counter; 215 mCounter.mAllocatedInstances.getAndIncrement(); 216 } 217 218 @Override finalize()219 public void finalize() { 220 mCounter.mAllocatedInstances.getAndDecrement(); 221 } 222 223 @Override onCreate(Context sysuiContext, Context pluginContext)224 public void onCreate(Context sysuiContext, Context pluginContext) { 225 mCounter.mCreatedInstances.getAndIncrement(); 226 } 227 228 @Override onDestroy()229 public void onDestroy() { 230 mCounter.mCreatedInstances.getAndDecrement(); 231 } 232 } 233 234 public class FakeListener implements PluginListener<TestPlugin> { 235 public boolean mAttachReturn = true; 236 public int mAttachedCount = 0; 237 public int mDetachedCount = 0; 238 public int mLoadCount = 0; 239 public int mUnloadCount = 0; 240 241 @Override onPluginAttached(PluginLifecycleManager<TestPlugin> manager)242 public boolean onPluginAttached(PluginLifecycleManager<TestPlugin> manager) { 243 mAttachedCount++; 244 assertEquals(PluginInstanceTest.this.mPluginInstance, manager); 245 return mAttachReturn; 246 } 247 248 @Override onPluginDetached(PluginLifecycleManager<TestPlugin> manager)249 public void onPluginDetached(PluginLifecycleManager<TestPlugin> manager) { 250 mDetachedCount++; 251 assertEquals(PluginInstanceTest.this.mPluginInstance, manager); 252 } 253 254 @Override onPluginLoaded( TestPlugin plugin, Context pluginContext, PluginLifecycleManager<TestPlugin> manager )255 public void onPluginLoaded( 256 TestPlugin plugin, 257 Context pluginContext, 258 PluginLifecycleManager<TestPlugin> manager 259 ) { 260 mLoadCount++; 261 TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get(); 262 if (expectedPlugin != null) { 263 assertEquals(expectedPlugin, plugin); 264 } 265 Context expectedContext = PluginInstanceTest.this.mPluginContext.get(); 266 if (expectedContext != null) { 267 assertEquals(expectedContext, pluginContext); 268 } 269 assertEquals(PluginInstanceTest.this.mPluginInstance, manager); 270 } 271 272 @Override onPluginUnloaded( TestPlugin plugin, PluginLifecycleManager<TestPlugin> manager )273 public void onPluginUnloaded( 274 TestPlugin plugin, 275 PluginLifecycleManager<TestPlugin> manager 276 ) { 277 mUnloadCount++; 278 TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get(); 279 if (expectedPlugin != null) { 280 assertEquals(expectedPlugin, plugin); 281 } 282 assertEquals(PluginInstanceTest.this.mPluginInstance, manager); 283 } 284 } 285 } 286