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