• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.content.Context;
20 import android.location.Location;
21 import android.net.Uri;
22 import android.os.OutcomeReceiver;
23 import android.os.PersistableBundle;
24 import android.os.UserHandle;
25 import android.provider.CallLog;
26 import android.telephony.CarrierConfigManager;
27 import android.telephony.TelephonyManager;
28 import android.telephony.gba.UaSecurityProtocolIdentifier;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.util.Pair;
32 import android.util.SparseArray;
33 
34 import androidx.annotation.NonNull;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.phone.R;
38 
39 import java.io.ByteArrayInputStream;
40 import java.io.ByteArrayOutputStream;
41 import java.io.InputStream;
42 import java.util.HashMap;
43 import java.util.UUID;
44 import java.util.concurrent.CompletableFuture;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.Executors;
47 import java.util.concurrent.ScheduledExecutorService;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.atomic.AtomicBoolean;
50 import java.util.function.Consumer;
51 
52 public class CallComposerPictureManager {
53     private static final String TAG = CallComposerPictureManager.class.getSimpleName();
54     private static final SparseArray<CallComposerPictureManager> sInstances = new SparseArray<>();
55     private static final String THREE_GPP_BOOTSTRAPPING = "3GPP-bootstrapping";
56 
getInstance(Context context, int subscriptionId)57     public static CallComposerPictureManager getInstance(Context context, int subscriptionId) {
58         synchronized (sInstances) {
59             if (sExecutorService == null) {
60                 sExecutorService = Executors.newSingleThreadScheduledExecutor();
61             }
62             if (!sInstances.contains(subscriptionId)) {
63                 sInstances.put(subscriptionId,
64                         new CallComposerPictureManager(context, subscriptionId));
65             }
66             return sInstances.get(subscriptionId);
67         }
68     }
69 
70     @VisibleForTesting
clearInstances()71     public static void clearInstances() {
72         synchronized (sInstances) {
73             sInstances.clear();
74             if (sExecutorService != null) {
75                 sExecutorService.shutdown();
76                 sExecutorService = null;
77             }
78         }
79     }
80 
81     // disabled provisionally until the auth stack is fully operational
82     @VisibleForTesting
83     public static boolean sTestMode = false;
84     public static final String FAKE_SERVER_URL = "https://example.com/FAKE.png";
85     public static final String FAKE_SUBJECT = "This is a test call subject";
86     public static final Location FAKE_LOCATION = new Location("");
87     static {
88         // Meteor Crater, AZ
89         FAKE_LOCATION.setLatitude(35.027526);
90         FAKE_LOCATION.setLongitude(-111.021696);
91     }
92 
93     public interface CallLogProxy {
storeCallComposerPictureAsUser(Context context, UserHandle user, InputStream input, Executor executor, OutcomeReceiver<Uri, CallLog.CallComposerLoggingException> callback)94         default void storeCallComposerPictureAsUser(Context context,
95                 UserHandle user,
96                 InputStream input,
97                 Executor executor,
98                 OutcomeReceiver<Uri, CallLog.CallComposerLoggingException> callback) {
99             CallLog.storeCallComposerPicture(context.createContextAsUser(user, 0),
100                     input, executor, callback);
101         }
102     }
103 
104     private static ScheduledExecutorService sExecutorService = null;
105 
106     private final HashMap<UUID, String> mCachedServerUrls = new HashMap<>();
107     private final HashMap<UUID, ImageData> mCachedImages = new HashMap<>();
108     private GbaCredentials mCachedCredentials = null;
109     private final int mSubscriptionId;
110     private final TelephonyManager mTelephonyManager;
111     private final Context mContext;
112     private CallLogProxy mCallLogProxy = new CallLogProxy() {};
113 
CallComposerPictureManager(Context context, int subscriptionId)114     private CallComposerPictureManager(Context context, int subscriptionId) {
115         mContext = context;
116         mSubscriptionId = subscriptionId;
117         mTelephonyManager = mContext.getSystemService(TelephonyManager.class)
118                 .createForSubscriptionId(mSubscriptionId);
119     }
120 
handleUploadToServer(CallComposerPictureTransfer.Factory transferFactory, ImageData imageData, Consumer<Pair<UUID, Integer>> callback)121     public void handleUploadToServer(CallComposerPictureTransfer.Factory transferFactory,
122             ImageData imageData, Consumer<Pair<UUID, Integer>> callback) {
123         if (sTestMode) {
124             UUID id = UUID.randomUUID();
125             mCachedImages.put(id, imageData);
126             mCachedServerUrls.put(id, FAKE_SERVER_URL);
127             callback.accept(Pair.create(id, TelephonyManager.CallComposerException.SUCCESS));
128             return;
129         }
130 
131         PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig();
132         String uploadUrl = carrierConfig.getString(
133                 CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
134         if (TextUtils.isEmpty(uploadUrl)) {
135             Log.e(TAG, "Call composer upload URL not configured in carrier config");
136             callback.accept(Pair.create(null,
137                     TelephonyManager.CallComposerException.ERROR_UNKNOWN));
138         }
139         UUID id = UUID.randomUUID();
140         imageData.setId(id.toString());
141 
142         CallComposerPictureTransfer transfer = transferFactory.create(mContext,
143                 mSubscriptionId, uploadUrl, sExecutorService);
144 
145         AtomicBoolean hasRetried = new AtomicBoolean(false);
146         transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() {
147             @Override
148             public void onError(int error) {
149                 callback.accept(Pair.create(null, error));
150             }
151 
152             @Override
153             public void onRetryNeeded(boolean credentialRefresh, long backoffMillis) {
154                 if (hasRetried.getAndSet(true)) {
155                     Log.e(TAG, "Giving up on image upload after one retry.");
156                     callback.accept(Pair.create(null,
157                             TelephonyManager.CallComposerException.ERROR_NETWORK_UNAVAILABLE));
158                     return;
159                 }
160                 GbaCredentialsSupplier supplier =
161                         (realm, executor) ->
162                                 getGbaCredentials(credentialRefresh, carrierConfig, executor);
163 
164                 sExecutorService.schedule(() -> transfer.uploadPicture(imageData, supplier),
165                         backoffMillis, TimeUnit.MILLISECONDS);
166             }
167 
168             @Override
169             public void onUploadSuccessful(String serverUrl) {
170                 mCachedServerUrls.put(id, serverUrl);
171                 mCachedImages.put(id, imageData);
172                 Log.i(TAG, "Successfully received url: " + serverUrl + " associated with "
173                         + id.toString());
174                 callback.accept(Pair.create(id, TelephonyManager.CallComposerException.SUCCESS));
175             }
176         });
177 
178         transfer.uploadPicture(imageData,
179                 (realm, executor) -> getGbaCredentials(false, carrierConfig, executor));
180     }
181 
handleDownloadFromServer(CallComposerPictureTransfer.Factory transferFactory, String remoteUrl, Consumer<Pair<Uri, Integer>> callback)182     public void handleDownloadFromServer(CallComposerPictureTransfer.Factory transferFactory,
183             String remoteUrl, Consumer<Pair<Uri, Integer>> callback) {
184         if (sTestMode) {
185             ImageData imageData = new ImageData(getPlaceholderPictureAsBytes(), "image/png", null);
186             UUID id = UUID.randomUUID();
187             mCachedImages.put(id, imageData);
188             storeUploadedPictureToCallLog(id, uri -> callback.accept(Pair.create(uri, -1)));
189             return;
190         }
191 
192         PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig();
193         CallComposerPictureTransfer transfer = transferFactory.create(mContext,
194                 mSubscriptionId, remoteUrl, sExecutorService);
195 
196         AtomicBoolean hasRetried = new AtomicBoolean(false);
197         transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() {
198             @Override
199             public void onError(int error) {
200                 callback.accept(Pair.create(null, error));
201             }
202 
203             @Override
204             public void onRetryNeeded(boolean credentialRefresh, long backoffMillis) {
205                 if (hasRetried.getAndSet(true)) {
206                     Log.e(TAG, "Giving up on image download after one retry.");
207                     callback.accept(Pair.create(null,
208                             TelephonyManager.CallComposerException.ERROR_NETWORK_UNAVAILABLE));
209                     return;
210                 }
211                 GbaCredentialsSupplier supplier =
212                         (realm, executor) ->
213                                 getGbaCredentials(credentialRefresh, carrierConfig, executor);
214 
215                 sExecutorService.schedule(() -> transfer.downloadPicture(supplier),
216                         backoffMillis, TimeUnit.MILLISECONDS);
217             }
218 
219             @Override
220             public void onDownloadSuccessful(ImageData data) {
221                 ByteArrayInputStream imageDataInput =
222                         new ByteArrayInputStream(data.getImageBytes());
223                 mCallLogProxy.storeCallComposerPictureAsUser(
224                         mContext, UserHandle.CURRENT, imageDataInput,
225                         sExecutorService,
226                         new OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>() {
227                             @Override
228                             public void onResult(@NonNull Uri result) {
229                                 callback.accept(Pair.create(
230                                         result, TelephonyManager.CallComposerException.SUCCESS));
231                             }
232 
233                             @Override
234                             public void onError(CallLog.CallComposerLoggingException e) {
235                                 // Just report an error to the client for now.
236                                 callback.accept(Pair.create(null,
237                                         TelephonyManager.CallComposerException.ERROR_UNKNOWN));
238                             }
239                         });
240             }
241         });
242 
243         transfer.downloadPicture(((realm, executor) ->
244                 getGbaCredentials(false, carrierConfig, executor)));
245     }
246 
storeUploadedPictureToCallLog(UUID id, Consumer<Uri> callback)247     public void storeUploadedPictureToCallLog(UUID id, Consumer<Uri> callback) {
248         ImageData data = mCachedImages.get(id);
249         if (data == null) {
250             Log.e(TAG, "No picture associated with uuid " + id);
251             callback.accept(null);
252             return;
253         }
254         ByteArrayInputStream imageDataInput =
255                 new ByteArrayInputStream(data.getImageBytes());
256         mCallLogProxy.storeCallComposerPictureAsUser(mContext, UserHandle.CURRENT, imageDataInput,
257                 sExecutorService,
258                 new OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>() {
259                     @Override
260                     public void onResult(@NonNull Uri result) {
261                         callback.accept(result);
262                         clearCachedData();
263                     }
264 
265                     @Override
266                     public void onError(CallLog.CallComposerLoggingException e) {
267                         // Just report an error to the client for now.
268                         Log.e(TAG, "Error logging uploaded image: " + e.getErrorCode());
269                         callback.accept(null);
270                         clearCachedData();
271                     }
272                 });
273     }
274 
getServerUrlForImageId(UUID id)275     public String getServerUrlForImageId(UUID id) {
276         return mCachedServerUrls.get(id);
277     }
278 
clearCachedData()279     public void clearCachedData() {
280         mCachedServerUrls.clear();
281         mCachedImages.clear();
282     }
283 
getPlaceholderPictureAsBytes()284     private byte[] getPlaceholderPictureAsBytes() {
285         InputStream resourceInput = mContext.getResources().openRawResource(R.drawable.cupcake);
286         try {
287             return readBytes(resourceInput);
288         } catch (Exception e) {
289             return new byte[] {};
290         }
291     }
292 
readBytes(InputStream inputStream)293     private static byte[] readBytes(InputStream inputStream) throws Exception {
294         byte[] buffer = new byte[1024];
295         ByteArrayOutputStream output = new ByteArrayOutputStream();
296         int numRead;
297         do {
298             numRead = inputStream.read(buffer);
299             if (numRead > 0) output.write(buffer, 0, numRead);
300         } while (numRead > 0);
301         return output.toByteArray();
302     }
303 
getGbaCredentials( boolean forceRefresh, PersistableBundle config, Executor executor)304     private CompletableFuture<GbaCredentials> getGbaCredentials(
305             boolean forceRefresh, PersistableBundle config, Executor executor) {
306         synchronized (this) {
307             if (!forceRefresh && mCachedCredentials != null) {
308                 return CompletableFuture.completedFuture(mCachedCredentials);
309             }
310 
311             if (forceRefresh) {
312                 mCachedCredentials = null;
313             }
314         }
315 
316         UaSecurityProtocolIdentifier securityProtocolIdentifier =
317                 new UaSecurityProtocolIdentifier.Builder()
318                         .setOrg(config.getInt(
319                                 CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT))
320                         .setProtocol(config.getInt(
321                                 CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT))
322                         .setTlsCipherSuite(config.getInt(
323                                 CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT))
324                         .build();
325         CompletableFuture<GbaCredentials> resultFuture = new CompletableFuture<>();
326 
327         mTelephonyManager.bootstrapAuthenticationRequest(TelephonyManager.APPTYPE_ISIM,
328                 getNafUri(config), securityProtocolIdentifier, forceRefresh, executor,
329                 new TelephonyManager.BootstrapAuthenticationCallback() {
330                     @Override
331                     public void onKeysAvailable(byte[] gbaKey, String transactionId) {
332                         GbaCredentials creds = new GbaCredentials(transactionId, gbaKey);
333                         synchronized (CallComposerPictureManager.this) {
334                             mCachedCredentials = creds;
335                         }
336                         resultFuture.complete(creds);
337                     }
338 
339                     @Override
340                     public void onAuthenticationFailure(int reason) {
341                         Log.e(TAG, "GBA auth failed: reason=" + reason);
342                         resultFuture.complete(null);
343                     }
344                 });
345 
346         return resultFuture;
347     }
348 
getNafUri(PersistableBundle carrierConfig)349     private static Uri getNafUri(PersistableBundle carrierConfig) {
350         String uploadUriString = carrierConfig.getString(
351                 CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
352         Uri uploadUri = Uri.parse(uploadUriString);
353         String nafPrefix;
354         switch (carrierConfig.getInt(CarrierConfigManager.KEY_GBA_MODE_INT)) {
355             case CarrierConfigManager.GBA_U:
356                 nafPrefix = THREE_GPP_BOOTSTRAPPING + "-uicc";
357                 break;
358             case CarrierConfigManager.GBA_DIGEST:
359                 nafPrefix = THREE_GPP_BOOTSTRAPPING + "-digest";
360                 break;
361             case CarrierConfigManager.GBA_ME:
362             default:
363                 nafPrefix = THREE_GPP_BOOTSTRAPPING;
364         }
365         String newAuthority = nafPrefix + "@" + uploadUri.getAuthority();
366         Uri nafUri = new Uri.Builder().scheme(uploadUri.getScheme())
367                 .encodedAuthority(newAuthority)
368                 .build();
369         Log.i(TAG, "using NAF uri " + nafUri + " for GBA");
370         return nafUri;
371     }
372 
373     @VisibleForTesting
getExecutor()374     static ScheduledExecutorService getExecutor() {
375         return sExecutorService;
376     }
377 
378     @VisibleForTesting
setCallLogProxy(CallLogProxy proxy)379     void setCallLogProxy(CallLogProxy proxy) {
380         mCallLogProxy = proxy;
381     }
382 }
383