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.phone.callcomposer; 18 19 import static org.junit.Assert.assertArrayEquals; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertNotNull; 22 import static org.mockito.ArgumentMatchers.anyInt; 23 import static org.mockito.ArgumentMatchers.eq; 24 import static org.mockito.ArgumentMatchers.nullable; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 29 import android.content.Context; 30 import android.net.Uri; 31 import android.os.OutcomeReceiver; 32 import android.os.PersistableBundle; 33 import android.os.UserHandle; 34 import android.provider.CallLog; 35 import android.telephony.CarrierConfigManager; 36 import android.telephony.TelephonyManager; 37 import android.telephony.gba.TlsParams; 38 import android.telephony.gba.UaSecurityProtocolIdentifier; 39 40 import org.junit.After; 41 import org.junit.Before; 42 import org.junit.Test; 43 import org.mockito.ArgumentCaptor; 44 import org.mockito.Mock; 45 import org.mockito.MockitoAnnotations; 46 47 import java.io.InputStream; 48 import java.util.UUID; 49 import java.util.concurrent.CompletableFuture; 50 import java.util.concurrent.ExecutionException; 51 import java.util.concurrent.Executor; 52 import java.util.concurrent.ExecutorService; 53 import java.util.concurrent.TimeUnit; 54 import java.util.concurrent.TimeoutException; 55 56 public class PictureManagerTest { 57 private static final String FAKE_URL_BASE = "https://www.example.com"; 58 private static final String FAKE_URL = "https://www.example.com/AAAAA"; 59 private static final long TIMEOUT_MILLIS = 1000; 60 private static final Uri FAKE_CALLLOG_URI = Uri.parse("content://asdf"); 61 62 @Mock CallComposerPictureManager.CallLogProxy mockCallLogProxy; 63 @Mock CallComposerPictureTransfer mockPictureTransfer; 64 @Mock Context context; 65 @Mock TelephonyManager telephonyManager; 66 67 private boolean originalTestMode = false; 68 @Before setUp()69 public void setUp() throws Exception { 70 MockitoAnnotations.initMocks(this); 71 originalTestMode = CallComposerPictureManager.sTestMode; 72 // Even though this is a test, we want test mode off so we can actually exercise the logic 73 // in the class. 74 CallComposerPictureManager.sTestMode = false; 75 when(context.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(telephonyManager); 76 when(context.getSystemServiceName(TelephonyManager.class)) 77 .thenReturn(Context.TELEPHONY_SERVICE); 78 when(telephonyManager.createForSubscriptionId(anyInt())).thenReturn(telephonyManager); 79 PersistableBundle b = new PersistableBundle(); 80 b.putString(CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING, 81 FAKE_URL_BASE); 82 b.putInt(CarrierConfigManager.KEY_GBA_MODE_INT, 83 CarrierConfigManager.GBA_ME); 84 b.putInt(CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT, 85 UaSecurityProtocolIdentifier.ORG_3GPP); 86 b.putInt(CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT, 87 UaSecurityProtocolIdentifier.UA_SECURITY_PROTOCOL_3GPP_TLS_DEFAULT); 88 b.putInt(CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT, 89 TlsParams.TLS_RSA_WITH_AES_128_CBC_SHA); 90 when(telephonyManager.getCarrierConfig()).thenReturn(b); 91 } 92 93 @After tearDown()94 public void tearDown() throws Exception { 95 CallComposerPictureManager.sTestMode = originalTestMode; 96 CallComposerPictureManager.clearInstances(); 97 } 98 99 @Test testPictureUpload()100 public void testPictureUpload() throws Exception { 101 CallComposerPictureManager manager = CallComposerPictureManager.getInstance(context, 0); 102 manager.setCallLogProxy(mockCallLogProxy); 103 ImageData imageData = new ImageData(new byte[] {1,2,3,4}, 104 "image/png", null); 105 106 CompletableFuture<UUID> uploadedUuidFuture = new CompletableFuture<>(); 107 manager.handleUploadToServer(new CallComposerPictureTransfer.Factory() { 108 @Override 109 public CallComposerPictureTransfer create(Context context, int subscriptionId, 110 String url, ExecutorService executorService) { 111 return mockPictureTransfer; 112 } 113 }, imageData, (pair) -> uploadedUuidFuture.complete(pair.first)); 114 115 // Get the callback for later manipulation 116 ArgumentCaptor<CallComposerPictureTransfer.PictureCallback> callbackCaptor = 117 ArgumentCaptor.forClass(CallComposerPictureTransfer.PictureCallback.class); 118 verify(mockPictureTransfer).setCallback(callbackCaptor.capture()); 119 120 // Make sure the upload method is called 121 ArgumentCaptor<GbaCredentialsSupplier> credSupplierCaptor = 122 ArgumentCaptor.forClass(GbaCredentialsSupplier.class); 123 ArgumentCaptor<ImageData> imageDataCaptor = 124 ArgumentCaptor.forClass(ImageData.class); 125 verify(mockPictureTransfer).uploadPicture(imageDataCaptor.capture(), 126 credSupplierCaptor.capture()); 127 128 // Make sure the id field on the image data got filled in 129 ImageData sentData = imageDataCaptor.getValue(); 130 assertArrayEquals(imageData.getImageBytes(), sentData.getImageBytes()); 131 assertNotNull(sentData.getId()); 132 String imageId = sentData.getId(); 133 134 testGbaCredLookup(credSupplierCaptor.getValue(), false); 135 136 // Trigger upload success, make sure that the internal state is consistent after the upload. 137 callbackCaptor.getValue().onUploadSuccessful(FAKE_URL); 138 UUID id = uploadedUuidFuture.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 139 assertEquals(imageId, id.toString()); 140 assertEquals(FAKE_URL, manager.getServerUrlForImageId(id)); 141 142 // Test the call log upload 143 CompletableFuture<Uri> callLogUriFuture = new CompletableFuture<>(); 144 manager.storeUploadedPictureToCallLog(id, callLogUriFuture::complete); 145 146 ArgumentCaptor<OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>> 147 callLogCallbackCaptor = ArgumentCaptor.forClass(OutcomeReceiver.class); 148 149 verify(mockCallLogProxy).storeCallComposerPictureAsUser(nullable(Context.class), 150 nullable(UserHandle.class), nullable(InputStream.class), nullable(Executor.class), 151 callLogCallbackCaptor.capture()); 152 callLogCallbackCaptor.getValue().onResult(FAKE_CALLLOG_URI); 153 Uri receivedUri = callLogUriFuture.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 154 assertEquals(FAKE_CALLLOG_URI, receivedUri); 155 } 156 157 @Test testPictureUploadWithAuthRefresh()158 public void testPictureUploadWithAuthRefresh() throws Exception { 159 CallComposerPictureManager manager = CallComposerPictureManager.getInstance(context, 0); 160 manager.setCallLogProxy(mockCallLogProxy); 161 ImageData imageData = new ImageData(new byte[] {1,2,3,4}, 162 "image/png", null); 163 164 CompletableFuture<UUID> uploadedUuidFuture = new CompletableFuture<>(); 165 manager.handleUploadToServer(new CallComposerPictureTransfer.Factory() { 166 @Override 167 public CallComposerPictureTransfer create(Context context, int subscriptionId, 168 String url, ExecutorService executorService) { 169 return mockPictureTransfer; 170 } 171 }, imageData, (pair) -> uploadedUuidFuture.complete(pair.first)); 172 173 // Get the callback for later manipulation 174 ArgumentCaptor<CallComposerPictureTransfer.PictureCallback> callbackCaptor = 175 ArgumentCaptor.forClass(CallComposerPictureTransfer.PictureCallback.class); 176 verify(mockPictureTransfer).setCallback(callbackCaptor.capture()); 177 178 // Make sure the upload method is called 179 verify(mockPictureTransfer).uploadPicture(nullable(ImageData.class), 180 nullable(GbaCredentialsSupplier.class)); 181 182 // Simulate a auth-needed retry request 183 callbackCaptor.getValue().onRetryNeeded(true, 0); 184 waitForExecutorAction(CallComposerPictureManager.getExecutor(), TIMEOUT_MILLIS); 185 186 // Make sure upload gets called again immediately, and make sure that the new GBA creds 187 // are requested with a force-refresh. 188 ArgumentCaptor<GbaCredentialsSupplier> credSupplierCaptor = 189 ArgumentCaptor.forClass(GbaCredentialsSupplier.class); 190 verify(mockPictureTransfer, times(2)).uploadPicture(nullable(ImageData.class), 191 credSupplierCaptor.capture()); 192 193 testGbaCredLookup(credSupplierCaptor.getValue(), true); 194 } 195 196 @Test testPictureDownload()197 public void testPictureDownload() throws Exception { 198 ImageData imageData = new ImageData(new byte[] {1,2,3,4}, 199 "image/png", null); 200 CallComposerPictureManager manager = CallComposerPictureManager.getInstance(context, 0); 201 manager.setCallLogProxy(mockCallLogProxy); 202 203 CompletableFuture<Uri> callLogUriFuture = new CompletableFuture<>(); 204 manager.handleDownloadFromServer(new CallComposerPictureTransfer.Factory() { 205 @Override 206 public CallComposerPictureTransfer create(Context context, int subscriptionId, 207 String url, ExecutorService executorService) { 208 return mockPictureTransfer; 209 } 210 }, FAKE_URL, (p) -> callLogUriFuture.complete(p.first)); 211 212 // Get the callback for later manipulation 213 ArgumentCaptor<CallComposerPictureTransfer.PictureCallback> callbackCaptor = 214 ArgumentCaptor.forClass(CallComposerPictureTransfer.PictureCallback.class); 215 verify(mockPictureTransfer).setCallback(callbackCaptor.capture()); 216 217 // Make sure the download method is called 218 ArgumentCaptor<GbaCredentialsSupplier> credSupplierCaptor = 219 ArgumentCaptor.forClass(GbaCredentialsSupplier.class); 220 verify(mockPictureTransfer).downloadPicture(credSupplierCaptor.capture()); 221 222 testGbaCredLookup(credSupplierCaptor.getValue(), false); 223 224 // Trigger download success, make sure that the call log is called into next. 225 callbackCaptor.getValue().onDownloadSuccessful(imageData); 226 ArgumentCaptor<OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>> 227 callLogCallbackCaptor = ArgumentCaptor.forClass(OutcomeReceiver.class); 228 verify(mockCallLogProxy).storeCallComposerPictureAsUser(nullable(Context.class), 229 nullable(UserHandle.class), nullable(InputStream.class), nullable(Executor.class), 230 callLogCallbackCaptor.capture()); 231 232 callLogCallbackCaptor.getValue().onResult(FAKE_CALLLOG_URI); 233 Uri receivedUri = callLogUriFuture.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 234 assertEquals(FAKE_CALLLOG_URI, receivedUri); 235 } 236 237 @Test testPictureDownloadWithAuthRefresh()238 public void testPictureDownloadWithAuthRefresh() throws Exception { 239 CallComposerPictureManager manager = CallComposerPictureManager.getInstance(context, 0); 240 manager.setCallLogProxy(mockCallLogProxy); 241 242 CompletableFuture<Uri> callLogUriFuture = new CompletableFuture<>(); 243 manager.handleDownloadFromServer(new CallComposerPictureTransfer.Factory() { 244 @Override 245 public CallComposerPictureTransfer create(Context context, int subscriptionId, 246 String url, ExecutorService executorService) { 247 return mockPictureTransfer; 248 } 249 }, FAKE_URL, (p) -> callLogUriFuture.complete(p.first)); 250 251 // Get the callback for later manipulation 252 ArgumentCaptor<CallComposerPictureTransfer.PictureCallback> callbackCaptor = 253 ArgumentCaptor.forClass(CallComposerPictureTransfer.PictureCallback.class); 254 verify(mockPictureTransfer).setCallback(callbackCaptor.capture()); 255 256 // Make sure the download method is called 257 verify(mockPictureTransfer).downloadPicture(nullable(GbaCredentialsSupplier.class)); 258 259 // Simulate a auth-needed retry request 260 callbackCaptor.getValue().onRetryNeeded(true, 0); 261 waitForExecutorAction(CallComposerPictureManager.getExecutor(), TIMEOUT_MILLIS); 262 263 // Make sure download gets called again immediately, and make sure that the new GBA creds 264 // are requested with a force-refresh. 265 ArgumentCaptor<GbaCredentialsSupplier> credSupplierCaptor = 266 ArgumentCaptor.forClass(GbaCredentialsSupplier.class); 267 verify(mockPictureTransfer, times(2)).downloadPicture(credSupplierCaptor.capture()); 268 269 testGbaCredLookup(credSupplierCaptor.getValue(), true); 270 } 271 272 testGbaCredLookup(GbaCredentialsSupplier supplier, boolean forceExpected)273 public void testGbaCredLookup(GbaCredentialsSupplier supplier, boolean forceExpected) 274 throws Exception { 275 String fakeNafId = "https://3GPP-bootstrapping@www.example.com"; 276 byte[] fakeKey = new byte[] {1, 2, 3, 4, 5}; 277 String fakeTxId = "89sdfjggf"; 278 279 ArgumentCaptor<TelephonyManager.BootstrapAuthenticationCallback> authCallbackCaptor = 280 ArgumentCaptor.forClass(TelephonyManager.BootstrapAuthenticationCallback.class); 281 282 CompletableFuture<GbaCredentials> credsFuture = 283 supplier.getCredentials(fakeNafId, CallComposerPictureManager.getExecutor()); 284 verify(telephonyManager).bootstrapAuthenticationRequest(anyInt(), 285 eq(Uri.parse(fakeNafId)), 286 nullable(UaSecurityProtocolIdentifier.class), eq(forceExpected), 287 nullable(Executor.class), 288 authCallbackCaptor.capture()); 289 authCallbackCaptor.getValue().onKeysAvailable(fakeKey, fakeTxId); 290 GbaCredentials creds = credsFuture.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 291 assertEquals(fakeTxId, creds.getTransactionId()); 292 assertArrayEquals(fakeKey, creds.getKey()); 293 294 295 // Do it again and see if we make another request, then make sure that matches up with what 296 // we expected. 297 CompletableFuture<GbaCredentials> credsFuture1 = 298 supplier.getCredentials(fakeNafId, CallComposerPictureManager.getExecutor()); 299 verify(telephonyManager, times(forceExpected ? 2 : 1)) 300 .bootstrapAuthenticationRequest(anyInt(), eq(Uri.parse(fakeNafId)), 301 nullable(UaSecurityProtocolIdentifier.class), 302 eq(forceExpected), 303 nullable(Executor.class), 304 authCallbackCaptor.capture()); 305 authCallbackCaptor.getValue().onKeysAvailable(fakeKey, fakeTxId); 306 GbaCredentials creds1 = credsFuture1.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 307 assertEquals(fakeTxId, creds1.getTransactionId()); 308 assertArrayEquals(fakeKey, creds1.getKey()); 309 } 310 waitForExecutorAction( ExecutorService executorService, long timeoutMillis)311 private static boolean waitForExecutorAction( 312 ExecutorService executorService, long timeoutMillis) { 313 CompletableFuture<Void> f = new CompletableFuture<>(); 314 executorService.execute(() -> f.complete(null)); 315 try { 316 f.get(timeoutMillis, TimeUnit.MILLISECONDS); 317 } catch (TimeoutException e) { 318 return false; 319 } catch (InterruptedException | ExecutionException e) { 320 throw new RuntimeException(e); 321 } 322 return true; 323 } 324 } 325