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