1 /* 2 * Copyright (C) 2023 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 android.service.dreams; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.Mockito.atLeastOnce; 22 import static org.mockito.Mockito.never; 23 import static org.mockito.Mockito.times; 24 import static org.mockito.Mockito.verify; 25 26 import android.content.Context; 27 import android.content.Intent; 28 import android.os.Handler; 29 import android.os.RemoteException; 30 import android.os.test.TestLooper; 31 32 import androidx.test.ext.junit.runners.AndroidJUnit4; 33 import androidx.test.filters.SmallTest; 34 35 import com.android.internal.util.ObservableServiceConnection; 36 import com.android.internal.util.PersistentServiceConnection; 37 38 import org.junit.Before; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 import org.mockito.ArgumentCaptor; 42 import org.mockito.Mock; 43 import org.mockito.Mockito; 44 import org.mockito.MockitoAnnotations; 45 46 import java.util.concurrent.atomic.AtomicInteger; 47 import java.util.function.Consumer; 48 49 @SmallTest 50 @RunWith(AndroidJUnit4.class) 51 public class DreamOverlayConnectionHandlerTest { 52 private static final int MIN_CONNECTION_DURATION_MS = 100; 53 private static final int MAX_RECONNECT_ATTEMPTS = 3; 54 private static final int BASE_RECONNECT_DELAY_MS = 50; 55 56 @Mock 57 private Context mContext; 58 @Mock 59 private PersistentServiceConnection<IDreamOverlay> mConnection; 60 @Mock 61 private Intent mServiceIntent; 62 @Mock 63 private IDreamOverlay mOverlayService; 64 @Mock 65 private IDreamOverlayClient mOverlayClient; 66 67 private TestLooper mTestLooper; 68 private DreamOverlayConnectionHandler mDreamOverlayConnectionHandler; 69 70 @Before setUp()71 public void setUp() { 72 MockitoAnnotations.initMocks(this); 73 mTestLooper = new TestLooper(); 74 mDreamOverlayConnectionHandler = new DreamOverlayConnectionHandler( 75 mContext, 76 mTestLooper.getLooper(), 77 mServiceIntent, 78 MIN_CONNECTION_DURATION_MS, 79 MAX_RECONNECT_ATTEMPTS, 80 BASE_RECONNECT_DELAY_MS, 81 new TestInjector(mConnection)); 82 } 83 84 @Test consumerShouldRunImmediatelyWhenClientAvailable()85 public void consumerShouldRunImmediatelyWhenClientAvailable() throws RemoteException { 86 mDreamOverlayConnectionHandler.bind(); 87 connectService(); 88 provideClient(); 89 90 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 91 mDreamOverlayConnectionHandler.addConsumer(consumer); 92 mTestLooper.dispatchAll(); 93 verify(consumer).accept(mOverlayClient); 94 } 95 96 @Test consumerShouldRunAfterClientAvailable()97 public void consumerShouldRunAfterClientAvailable() throws RemoteException { 98 mDreamOverlayConnectionHandler.bind(); 99 connectService(); 100 101 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 102 mDreamOverlayConnectionHandler.addConsumer(consumer); 103 mTestLooper.dispatchAll(); 104 // No client yet, so we shouldn't have executed 105 verify(consumer, never()).accept(mOverlayClient); 106 107 provideClient(); 108 mTestLooper.dispatchAll(); 109 verify(consumer).accept(mOverlayClient); 110 } 111 112 @Test consumerShouldNeverRunIfClientConnectsAndDisconnects()113 public void consumerShouldNeverRunIfClientConnectsAndDisconnects() throws RemoteException { 114 mDreamOverlayConnectionHandler.bind(); 115 connectService(); 116 117 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 118 mDreamOverlayConnectionHandler.addConsumer(consumer); 119 mTestLooper.dispatchAll(); 120 // No client yet, so we shouldn't have executed 121 verify(consumer, never()).accept(mOverlayClient); 122 123 provideClient(); 124 // Service disconnected before looper could handle the message. 125 disconnectService(); 126 mTestLooper.dispatchAll(); 127 verify(consumer, never()).accept(mOverlayClient); 128 } 129 130 @Test consumerShouldNeverRunIfUnbindCalled()131 public void consumerShouldNeverRunIfUnbindCalled() throws RemoteException { 132 mDreamOverlayConnectionHandler.bind(); 133 connectService(); 134 provideClient(); 135 136 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 137 mDreamOverlayConnectionHandler.addConsumer(consumer); 138 mDreamOverlayConnectionHandler.unbind(); 139 mTestLooper.dispatchAll(); 140 // We unbinded immediately after adding consumer, so should never have run. 141 verify(consumer, never()).accept(mOverlayClient); 142 } 143 144 @Test consumersOnlyRunOnceIfUnbound()145 public void consumersOnlyRunOnceIfUnbound() throws RemoteException { 146 mDreamOverlayConnectionHandler.bind(); 147 connectService(); 148 provideClient(); 149 150 AtomicInteger counter = new AtomicInteger(); 151 // Add 10 consumers in a row which call unbind within the consumer. 152 for (int i = 0; i < 10; i++) { 153 mDreamOverlayConnectionHandler.addConsumer(client -> { 154 counter.getAndIncrement(); 155 mDreamOverlayConnectionHandler.unbind(); 156 }); 157 } 158 mTestLooper.dispatchAll(); 159 // Only the first consumer should have run, since we unbinded. 160 assertThat(counter.get()).isEqualTo(1); 161 } 162 163 @Test consumerShouldRunAgainAfterReconnect()164 public void consumerShouldRunAgainAfterReconnect() throws RemoteException { 165 mDreamOverlayConnectionHandler.bind(); 166 connectService(); 167 provideClient(); 168 169 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 170 mDreamOverlayConnectionHandler.addConsumer(consumer); 171 mTestLooper.dispatchAll(); 172 verify(consumer, times(1)).accept(mOverlayClient); 173 174 disconnectService(); 175 mTestLooper.dispatchAll(); 176 // No new calls should happen when service disconnected. 177 verify(consumer, times(1)).accept(mOverlayClient); 178 179 connectService(); 180 provideClient(); 181 mTestLooper.dispatchAll(); 182 // We should trigger the consumer again once the server reconnects. 183 verify(consumer, times(2)).accept(mOverlayClient); 184 } 185 186 @Test consumerShouldNeverRunIfRemovedImmediately()187 public void consumerShouldNeverRunIfRemovedImmediately() throws RemoteException { 188 mDreamOverlayConnectionHandler.bind(); 189 connectService(); 190 provideClient(); 191 192 final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); 193 mDreamOverlayConnectionHandler.addConsumer(consumer); 194 mDreamOverlayConnectionHandler.removeConsumer(consumer); 195 mTestLooper.dispatchAll(); 196 verify(consumer, never()).accept(mOverlayClient); 197 } 198 connectService()199 private void connectService() { 200 final ObservableServiceConnection.Callback<IDreamOverlay> callback = 201 captureConnectionCallback(); 202 callback.onConnected(mConnection, mOverlayService); 203 } 204 disconnectService()205 private void disconnectService() { 206 final ObservableServiceConnection.Callback<IDreamOverlay> callback = 207 captureConnectionCallback(); 208 callback.onDisconnected(mConnection, /* reason= */ 0); 209 } 210 provideClient()211 private void provideClient() throws RemoteException { 212 final IDreamOverlayClientCallback callback = captureClientCallback(); 213 callback.onDreamOverlayClient(mOverlayClient); 214 } 215 captureConnectionCallback()216 private ObservableServiceConnection.Callback<IDreamOverlay> captureConnectionCallback() { 217 ArgumentCaptor<ObservableServiceConnection.Callback<IDreamOverlay>> 218 callbackCaptor = 219 ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); 220 verify(mConnection).addCallback(callbackCaptor.capture()); 221 return callbackCaptor.getValue(); 222 } 223 captureClientCallback()224 private IDreamOverlayClientCallback captureClientCallback() throws RemoteException { 225 ArgumentCaptor<IDreamOverlayClientCallback> callbackCaptor = 226 ArgumentCaptor.forClass(IDreamOverlayClientCallback.class); 227 verify(mOverlayService, atLeastOnce()).getClient(callbackCaptor.capture()); 228 return callbackCaptor.getValue(); 229 } 230 231 static class TestInjector extends DreamOverlayConnectionHandler.Injector { 232 private final PersistentServiceConnection<IDreamOverlay> mConnection; 233 TestInjector(PersistentServiceConnection<IDreamOverlay> connection)234 TestInjector(PersistentServiceConnection<IDreamOverlay> connection) { 235 mConnection = connection; 236 } 237 238 @Override buildConnection(Context context, Handler handler, Intent serviceIntent, int minConnectionDurationMs, int maxReconnectAttempts, int baseReconnectDelayMs)239 public PersistentServiceConnection<IDreamOverlay> buildConnection(Context context, 240 Handler handler, Intent serviceIntent, int minConnectionDurationMs, 241 int maxReconnectAttempts, int baseReconnectDelayMs) { 242 return mConnection; 243 } 244 } 245 } 246