/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.phone.callcomposer; import android.content.Context; import android.location.Location; import android.net.Uri; import android.os.OutcomeReceiver; import android.os.PersistableBundle; import android.os.UserHandle; import android.provider.CallLog; import android.telephony.CarrierConfigManager; import android.telephony.TelephonyManager; import android.telephony.gba.UaSecurityProtocolIdentifier; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.phone.R; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; public class CallComposerPictureManager { private static final String TAG = CallComposerPictureManager.class.getSimpleName(); private static final SparseArray sInstances = new SparseArray<>(); private static final String THREE_GPP_BOOTSTRAPPING = "3GPP-bootstrapping"; public static CallComposerPictureManager getInstance(Context context, int subscriptionId) { synchronized (sInstances) { if (sExecutorService == null) { sExecutorService = Executors.newSingleThreadScheduledExecutor(); } if (!sInstances.contains(subscriptionId)) { sInstances.put(subscriptionId, new CallComposerPictureManager(context, subscriptionId)); } return sInstances.get(subscriptionId); } } @VisibleForTesting public static void clearInstances() { synchronized (sInstances) { sInstances.clear(); if (sExecutorService != null) { sExecutorService.shutdown(); sExecutorService = null; } } } // disabled provisionally until the auth stack is fully operational @VisibleForTesting public static boolean sTestMode = false; public static final String FAKE_SERVER_URL = "https://example.com/FAKE.png"; public static final String FAKE_SUBJECT = "This is a test call subject"; public static final Location FAKE_LOCATION = new Location(""); static { // Meteor Crater, AZ FAKE_LOCATION.setLatitude(35.027526); FAKE_LOCATION.setLongitude(-111.021696); } public interface CallLogProxy { default void storeCallComposerPictureAsUser(Context context, UserHandle user, InputStream input, Executor executor, OutcomeReceiver callback) { CallLog.storeCallComposerPicture(context.createContextAsUser(user, 0), input, executor, callback); } } private static ScheduledExecutorService sExecutorService = null; private final HashMap mCachedServerUrls = new HashMap<>(); private final HashMap mCachedImages = new HashMap<>(); private GbaCredentials mCachedCredentials = null; private final int mSubscriptionId; private final TelephonyManager mTelephonyManager; private final Context mContext; private CallLogProxy mCallLogProxy = new CallLogProxy() {}; private CallComposerPictureManager(Context context, int subscriptionId) { mContext = context; mSubscriptionId = subscriptionId; mTelephonyManager = mContext.getSystemService(TelephonyManager.class) .createForSubscriptionId(mSubscriptionId); } public void handleUploadToServer(CallComposerPictureTransfer.Factory transferFactory, ImageData imageData, Consumer> callback) { if (sTestMode) { UUID id = UUID.randomUUID(); mCachedImages.put(id, imageData); mCachedServerUrls.put(id, FAKE_SERVER_URL); callback.accept(Pair.create(id, TelephonyManager.CallComposerException.SUCCESS)); return; } PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig(); String uploadUrl = carrierConfig.getString( CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING); if (TextUtils.isEmpty(uploadUrl)) { Log.e(TAG, "Call composer upload URL not configured in carrier config"); callback.accept(Pair.create(null, TelephonyManager.CallComposerException.ERROR_UNKNOWN)); } UUID id = UUID.randomUUID(); imageData.setId(id.toString()); CallComposerPictureTransfer transfer = transferFactory.create(mContext, mSubscriptionId, uploadUrl, sExecutorService); AtomicBoolean hasRetried = new AtomicBoolean(false); transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() { @Override public void onError(int error) { callback.accept(Pair.create(null, error)); } @Override public void onRetryNeeded(boolean credentialRefresh, long backoffMillis) { if (hasRetried.getAndSet(true)) { Log.e(TAG, "Giving up on image upload after one retry."); callback.accept(Pair.create(null, TelephonyManager.CallComposerException.ERROR_NETWORK_UNAVAILABLE)); return; } GbaCredentialsSupplier supplier = (realm, executor) -> getGbaCredentials(credentialRefresh, carrierConfig, executor); sExecutorService.schedule(() -> transfer.uploadPicture(imageData, supplier), backoffMillis, TimeUnit.MILLISECONDS); } @Override public void onUploadSuccessful(String serverUrl) { mCachedServerUrls.put(id, serverUrl); mCachedImages.put(id, imageData); Log.i(TAG, "Successfully received url: " + serverUrl + " associated with " + id.toString()); callback.accept(Pair.create(id, TelephonyManager.CallComposerException.SUCCESS)); } }); transfer.uploadPicture(imageData, (realm, executor) -> getGbaCredentials(false, carrierConfig, executor)); } public void handleDownloadFromServer(CallComposerPictureTransfer.Factory transferFactory, String remoteUrl, Consumer> callback) { if (sTestMode) { ImageData imageData = new ImageData(getPlaceholderPictureAsBytes(), "image/png", null); UUID id = UUID.randomUUID(); mCachedImages.put(id, imageData); storeUploadedPictureToCallLog(id, uri -> callback.accept(Pair.create(uri, -1))); return; } PersistableBundle carrierConfig = mTelephonyManager.getCarrierConfig(); CallComposerPictureTransfer transfer = transferFactory.create(mContext, mSubscriptionId, remoteUrl, sExecutorService); AtomicBoolean hasRetried = new AtomicBoolean(false); transfer.setCallback(new CallComposerPictureTransfer.PictureCallback() { @Override public void onError(int error) { callback.accept(Pair.create(null, error)); } @Override public void onRetryNeeded(boolean credentialRefresh, long backoffMillis) { if (hasRetried.getAndSet(true)) { Log.e(TAG, "Giving up on image download after one retry."); callback.accept(Pair.create(null, TelephonyManager.CallComposerException.ERROR_NETWORK_UNAVAILABLE)); return; } GbaCredentialsSupplier supplier = (realm, executor) -> getGbaCredentials(credentialRefresh, carrierConfig, executor); sExecutorService.schedule(() -> transfer.downloadPicture(supplier), backoffMillis, TimeUnit.MILLISECONDS); } @Override public void onDownloadSuccessful(ImageData data) { ByteArrayInputStream imageDataInput = new ByteArrayInputStream(data.getImageBytes()); mCallLogProxy.storeCallComposerPictureAsUser( mContext, UserHandle.CURRENT, imageDataInput, sExecutorService, new OutcomeReceiver() { @Override public void onResult(@NonNull Uri result) { callback.accept(Pair.create( result, TelephonyManager.CallComposerException.SUCCESS)); } @Override public void onError(CallLog.CallComposerLoggingException e) { // Just report an error to the client for now. callback.accept(Pair.create(null, TelephonyManager.CallComposerException.ERROR_UNKNOWN)); } }); } }); transfer.downloadPicture(((realm, executor) -> getGbaCredentials(false, carrierConfig, executor))); } public void storeUploadedPictureToCallLog(UUID id, Consumer callback) { ImageData data = mCachedImages.get(id); if (data == null) { Log.e(TAG, "No picture associated with uuid " + id); callback.accept(null); return; } ByteArrayInputStream imageDataInput = new ByteArrayInputStream(data.getImageBytes()); mCallLogProxy.storeCallComposerPictureAsUser(mContext, UserHandle.CURRENT, imageDataInput, sExecutorService, new OutcomeReceiver() { @Override public void onResult(@NonNull Uri result) { callback.accept(result); clearCachedData(); } @Override public void onError(CallLog.CallComposerLoggingException e) { // Just report an error to the client for now. Log.e(TAG, "Error logging uploaded image: " + e.getErrorCode()); callback.accept(null); clearCachedData(); } }); } public String getServerUrlForImageId(UUID id) { return mCachedServerUrls.get(id); } public void clearCachedData() { mCachedServerUrls.clear(); mCachedImages.clear(); } private byte[] getPlaceholderPictureAsBytes() { InputStream resourceInput = mContext.getResources().openRawResource(R.drawable.cupcake); try { return readBytes(resourceInput); } catch (Exception e) { return new byte[] {}; } } private static byte[] readBytes(InputStream inputStream) throws Exception { byte[] buffer = new byte[1024]; ByteArrayOutputStream output = new ByteArrayOutputStream(); int numRead; do { numRead = inputStream.read(buffer); if (numRead > 0) output.write(buffer, 0, numRead); } while (numRead > 0); return output.toByteArray(); } private CompletableFuture getGbaCredentials( boolean forceRefresh, PersistableBundle config, Executor executor) { synchronized (this) { if (!forceRefresh && mCachedCredentials != null) { return CompletableFuture.completedFuture(mCachedCredentials); } if (forceRefresh) { mCachedCredentials = null; } } UaSecurityProtocolIdentifier securityProtocolIdentifier = new UaSecurityProtocolIdentifier.Builder() .setOrg(config.getInt( CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT)) .setProtocol(config.getInt( CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT)) .setTlsCipherSuite(config.getInt( CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT)) .build(); CompletableFuture resultFuture = new CompletableFuture<>(); mTelephonyManager.bootstrapAuthenticationRequest(TelephonyManager.APPTYPE_ISIM, getNafUri(config), securityProtocolIdentifier, forceRefresh, executor, new TelephonyManager.BootstrapAuthenticationCallback() { @Override public void onKeysAvailable(byte[] gbaKey, String transactionId) { GbaCredentials creds = new GbaCredentials(transactionId, gbaKey); synchronized (CallComposerPictureManager.this) { mCachedCredentials = creds; } resultFuture.complete(creds); } @Override public void onAuthenticationFailure(int reason) { Log.e(TAG, "GBA auth failed: reason=" + reason); resultFuture.complete(null); } }); return resultFuture; } private static Uri getNafUri(PersistableBundle carrierConfig) { String uploadUriString = carrierConfig.getString( CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING); Uri uploadUri = Uri.parse(uploadUriString); String nafPrefix; switch (carrierConfig.getInt(CarrierConfigManager.KEY_GBA_MODE_INT)) { case CarrierConfigManager.GBA_U: nafPrefix = THREE_GPP_BOOTSTRAPPING + "-uicc"; break; case CarrierConfigManager.GBA_DIGEST: nafPrefix = THREE_GPP_BOOTSTRAPPING + "-digest"; break; case CarrierConfigManager.GBA_ME: default: nafPrefix = THREE_GPP_BOOTSTRAPPING; } String newAuthority = nafPrefix + "@" + uploadUri.getAuthority(); Uri nafUri = new Uri.Builder().scheme(uploadUri.getScheme()) .encodedAuthority(newAuthority) .build(); Log.i(TAG, "using NAF uri " + nafUri + " for GBA"); return nafUri; } @VisibleForTesting static ScheduledExecutorService getExecutor() { return sExecutorService; } @VisibleForTesting void setCallLogProxy(CallLogProxy proxy) { mCallLogProxy = proxy; } }