blob: b1fc47367b33f123fa6b270069032b2da63d1112 [file] [log] [blame]
/*
* Copyright (C) 2021 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.internal.telephony.uicc;
import android.annotation.RequiresFeature;
import android.content.Context;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.Message;
import android.telephony.Rlog;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.RadioInterfaceCapabilityController;
import com.android.internal.telephony.uicc.AdnCapacity;
import com.android.internal.telephony.uicc.IccConstants;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Used to store SIM phonebook records.
* <p/>
* This will be {@link #INVALID} if either is the case:
* <ol>
* <li>The device does not support
* {@link android.telephony.TelephonyManager#CAPABILITY_SIM_PHONEBOOK_IN_MODEM}.</li>
* </ol>
* {@hide}
*/
@RequiresFeature(
enforcement = "android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported",
value = "TelephonyManager.CAPABILITY_SIM_PHONEBOOK_IN_MODEM")
public class SimPhonebookRecordCache extends Handler {
// Instance Variables
private String LOG_TAG = "SimPhonebookRecordCache";
private static final boolean DBG = true;
@VisibleForTesting
static final boolean ENABLE_INFLATE_WITH_EMPTY_RECORDS = true;
// Event Constants
private static final int EVENT_PHONEBOOK_CHANGED = 1;
private static final int EVENT_PHONEBOOK_RECORDS_RECEIVED = 2;
private static final int EVENT_GET_PHONEBOOK_RECORDS_DONE = 3;
private static final int EVENT_GET_PHONEBOOK_CAPACITY_DONE = 4;
private static final int EVENT_UPDATE_PHONEBOOK_RECORD_DONE = 5;
private static final int EVENT_SIM_REFRESH = 6;
private static final int EVENT_GET_PHONEBOOK_RECORDS_RETRY = 7;
private static final int MAX_RETRY_COUNT = 3;
private static final int RETRY_INTERVAL = 3000; // 3S
private static final int INVALID_RECORD_ID = -1;
// member variables
private final CommandsInterface mCi;
private int mPhoneId;
private Context mContext;
// Presenting ADN capacity, including ADN, EMAIL ANR, and so on.
private AtomicReference<AdnCapacity> mAdnCapacity = new AtomicReference<AdnCapacity>(null);
private Object mReadLock = new Object();
private final ConcurrentSkipListMap<Integer, AdnRecord> mSimPbRecords =
new ConcurrentSkipListMap<Integer, AdnRecord>();
private final List<UpdateRequest> mUpdateRequests =
Collections.synchronizedList(new ArrayList<UpdateRequest>());
// If true, clear the records in the cache and re-query from modem
private AtomicBoolean mIsCacheInvalidated = new AtomicBoolean(false);
private AtomicBoolean mIsRecordLoading = new AtomicBoolean(false);
private AtomicBoolean mIsInRetry = new AtomicBoolean(false);
private AtomicBoolean mIsInitialized = new AtomicBoolean(false);
// People waiting for SIM phonebook records to be loaded
ArrayList<Message> mAdnLoadingWaiters = new ArrayList<Message>();
/**
* The manual update from upper layer will result in notifying SIM phonebook changed,
* leading to fetch the Adn capacity, then whether to need to reload phonebook records
* is a problem. the SIM phoneback changed shall follow by updating record done, so that
* uses this flag to avoid unnecessary loading.
*/
boolean mIsUpdateDone = false;
public SimPhonebookRecordCache(Context context, int phoneId, CommandsInterface ci) {
mCi = ci;
mPhoneId = phoneId;
mContext = context;
LOG_TAG += "[" + phoneId + "]";
mCi.registerForSimPhonebookChanged(this, EVENT_PHONEBOOK_CHANGED, null);
mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null);
mCi.registerForSimPhonebookRecordsReceived(this, EVENT_PHONEBOOK_RECORDS_RECEIVED, null);
}
/**
* This is recommended to use in work thread like IccPhoneBookInterfaceManager
* because it can't block main thread.
* @return true if this feature is supported
*/
public boolean isEnabled() {
boolean isEnabled = RadioInterfaceCapabilityController
.getInstance()
.getCapabilities()
.contains(TelephonyManager.CAPABILITY_SIM_PHONEBOOK_IN_MODEM);
return mIsInitialized.get() || isEnabled;
}
public void dispose() {
reset();
mCi.unregisterForSimPhonebookChanged(this);
mCi.unregisterForIccRefresh(this);
mCi.unregisterForSimPhonebookRecordsReceived(this);
}
private void reset() {
mAdnCapacity.set(null);
mSimPbRecords.clear();
mIsCacheInvalidated.set(false);
mIsRecordLoading.set(false);
mIsInRetry.set(false);
mIsInitialized.set(false);
mIsUpdateDone = false;
}
private void sendErrorResponse(Message response, String errString) {
if (response != null) {
Exception e = new RuntimeException(errString);
AsyncResult.forMessage(response).exception = e;
response.sendToTarget();
}
}
private void notifyAndClearWaiters() {
synchronized (mReadLock) {
for (Message response : mAdnLoadingWaiters){
if (response != null) {
List<AdnRecord> result =
new ArrayList<AdnRecord>(mSimPbRecords.values());
AsyncResult.forMessage(response, result, null);
response.sendToTarget();
}
}
mAdnLoadingWaiters.clear();
}
}
private void sendResponsesToWaitersWithError() {
synchronized (mReadLock) {
mReadLock.notify();
for (Message response : mAdnLoadingWaiters) {
sendErrorResponse(response, "Query adn record failed");
}
mAdnLoadingWaiters.clear();
}
}
private void getSimPhonebookCapacity() {
logd("Start to getSimPhonebookCapacity");
mCi.getSimPhonebookCapacity(obtainMessage(EVENT_GET_PHONEBOOK_CAPACITY_DONE));
}
public AdnCapacity getAdnCapacity() {
return mAdnCapacity.get();
}
private void fillCache() {
synchronized (mReadLock) {
fillCacheWithoutWaiting();
try {
mReadLock.wait();
} catch (InterruptedException e) {
loge("Interrupted Exception in queryAdnRecord");
}
}
}
private void fillCacheWithoutWaiting() {
logd("Start to queryAdnRecord");
if (mIsRecordLoading.compareAndSet(false, true)) {
mCi.getSimPhonebookRecords(obtainMessage(EVENT_GET_PHONEBOOK_RECORDS_DONE));
} else {
logd("The loading is ongoing");
}
}
public void requestLoadAllPbRecords(Message response) {
if (response == null && !mIsInitialized.get()) {
logd("Try to enforce flushing cache");
fillCacheWithoutWaiting();
return;
}
synchronized (mReadLock) {
mAdnLoadingWaiters.add(response);
final int pendingSize = mAdnLoadingWaiters.size();
final boolean isCapacityInvalid = isAdnCapacityInvalid();
if (isCapacityInvalid) {
getSimPhonebookCapacity();
}
if (pendingSize > 1 || mIsInRetry.get()
|| !mIsInitialized.get() || isCapacityInvalid) {
logd("Add to the pending list as pending size = "
+ pendingSize + " is retrying = " + mIsInRetry.get()
+ " IsInitialized = " + mIsInitialized.get());
return;
}
}
if (!mIsRecordLoading.get() && !mIsInRetry.get()) {
logd("ADN cache has already filled in");
if (!mIsCacheInvalidated.get()) {
notifyAndClearWaiters();
return;
}
}
fillCache();
}
private boolean isAdnCapacityInvalid() {
return getAdnCapacity() == null || !getAdnCapacity().isSimValid();
}
@VisibleForTesting
public boolean isLoading() {
return mIsRecordLoading.get();
}
@VisibleForTesting
public List<AdnRecord> getAdnRecords() {
return mSimPbRecords.values().stream().collect(Collectors.toList());
}
@VisibleForTesting
public void clear() {
if (!ENABLE_INFLATE_WITH_EMPTY_RECORDS) {
mSimPbRecords.clear();
}
}
private void notifyAdnLoadingWaiters() {
synchronized (mReadLock) {
mReadLock.notify();
}
notifyAndClearWaiters();
}
public void updateSimPbAdnByRecordId(int recordId, AdnRecord newAdn, Message response) {
if (newAdn == null) {
sendErrorResponse(response, "There is an invalid new Adn for update");
return;
}
boolean found = mSimPbRecords.containsKey(recordId);
if (!found) {
sendErrorResponse(response, "There is an invalid old Adn for update");
return;
}
updateSimPhonebookByNewAdn(recordId, newAdn, response);
}
public void updateSimPbAdnBySearch(AdnRecord oldAdn, AdnRecord newAdn, Message response) {
if (newAdn == null) {
sendErrorResponse(response, "There is an invalid new Adn for update");
return;
}
int recordId = INVALID_RECORD_ID; // The ID isn't specified by caller
if (oldAdn != null && !oldAdn.isEmpty()) {
for(AdnRecord adn : mSimPbRecords.values()) {
if (oldAdn.isEqual(adn)) {
recordId = adn.getRecId();
break;
}
}
}
if (recordId == INVALID_RECORD_ID
&& mAdnCapacity.get() != null && mAdnCapacity.get().isSimFull()) {
sendErrorResponse(response, "SIM Phonebook record is full");
return;
}
updateSimPhonebookByNewAdn(recordId, newAdn, response);
}
private void updateSimPhonebookByNewAdn(int recordId, AdnRecord newAdn, Message response) {
logd("update sim contact for record ID = " + recordId);
final int updatingRecordId = recordId == INVALID_RECORD_ID ? 0 : recordId;
SimPhonebookRecord updateAdn = new SimPhonebookRecord.Builder()
.setRecordId(updatingRecordId)
.setAlphaTag(newAdn.getAlphaTag())
.setNumber(newAdn.getNumber())
.setEmails(newAdn.getEmails())
.setAdditionalNumbers(newAdn.getAdditionalNumbers())
.build();
UpdateRequest updateRequest = new UpdateRequest(recordId, newAdn, updateAdn, response);
mUpdateRequests.add(updateRequest);
final boolean isCapacityInvalid = isAdnCapacityInvalid();
if (isCapacityInvalid) {
getSimPhonebookCapacity();
}
if (mIsRecordLoading.get() || mIsInRetry.get() || mUpdateRequests.size() > 1
|| !mIsInitialized.get() || isCapacityInvalid) {
logd("It is pending on update as " + " mIsRecordLoading = " + mIsRecordLoading.get()
+ " mIsInRetry = " + mIsInRetry.get() + " pending size = "
+ mUpdateRequests.size() + " mIsInitialized = " + mIsInitialized.get());
return;
}
updateSimPhonebook(updateRequest);
}
private void updateSimPhonebook(UpdateRequest request) {
logd("update Sim phonebook");
mCi.updateSimPhonebookRecord(request.phonebookRecord,
obtainMessage(EVENT_UPDATE_PHONEBOOK_RECORD_DONE, request));
}
@Override
public void handleMessage(Message msg) {
AsyncResult ar;
switch(msg.what) {
case EVENT_PHONEBOOK_CHANGED:
logd("EVENT_PHONEBOOK_CHANGED");
handlePhonebookChanged();
break;
case EVENT_GET_PHONEBOOK_RECORDS_DONE:
logd("EVENT_GET_PHONEBOOK_RECORDS_DONE");
ar = (AsyncResult)msg.obj;
if (ar != null && ar.exception != null) {
loge("Failed to gain phonebook records");
invalidateSimPbCache();
if (!mIsInRetry.get()) {
sendGettingPhonebookRecordsRetry(0);
}
}
break;
case EVENT_GET_PHONEBOOK_CAPACITY_DONE:
logd("EVENT_GET_PHONEBOOK_CAPACITY_DONE");
ar = (AsyncResult)msg.obj;
if (ar != null && ar.exception == null) {
AdnCapacity capacity = (AdnCapacity)ar.result;
handlePhonebookCapacityChanged(capacity);
} else {
if (!isAdnCapacityInvalid()) {
mAdnCapacity.set(new AdnCapacity());
}
invalidateSimPbCache();
}
break;
case EVENT_PHONEBOOK_RECORDS_RECEIVED:
logd("EVENT_PHONEBOOK_RECORDS_RECEIVED");
ar = (AsyncResult)msg.obj;
if (ar.exception != null) {
loge("Unexpected exception happened");
ar.result = null;
}
handlePhonebookRecordReceived((ReceivedPhonebookRecords)(ar.result));
break;
case EVENT_UPDATE_PHONEBOOK_RECORD_DONE:
logd("EVENT_UPDATE_PHONEBOOK_RECORD_DONE");
ar = (AsyncResult)msg.obj;
handleUpdatePhonebookRecordDone(ar);
break;
case EVENT_SIM_REFRESH:
logd("EVENT_SIM_REFRESH");
ar = (AsyncResult)msg.obj;
if (ar.exception == null) {
handleSimRefresh((IccRefreshResponse)ar.result);
} else {
logd("SIM refresh Exception: " + ar.exception);
}
break;
case EVENT_GET_PHONEBOOK_RECORDS_RETRY:
int retryCount = msg.arg1;
logd("EVENT_GET_PHONEBOOK_RECORDS_RETRY cnt = " + retryCount);
if (retryCount < MAX_RETRY_COUNT) {
mIsRecordLoading.set(false);
fillCacheWithoutWaiting();
sendGettingPhonebookRecordsRetry(++retryCount);
} else {
responseToWaitersWithErrorOrSuccess(false);
}
break;
default:
loge("Unexpected event: " + msg.what);
}
}
private void responseToWaitersWithErrorOrSuccess(boolean success) {
logd("responseToWaitersWithErrorOrSuccess success = " + success);
mIsRecordLoading.set(false);
mIsInRetry.set(false);
if (success) {
notifyAdnLoadingWaiters();
} else {
sendResponsesToWaitersWithError();
}
tryFireUpdatePendingList();
}
private void handlePhonebookChanged() {
if (mUpdateRequests.isEmpty()) {
// If this event is received, means this feature is supported.
getSimPhonebookCapacity();
} else {
logd("Do nothing in the midst of multiple update");
}
}
private void handlePhonebookCapacityChanged(AdnCapacity newCapacity) {
AdnCapacity oldCapacity = mAdnCapacity.get();
if (newCapacity == null) {
newCapacity = new AdnCapacity();
}
mAdnCapacity.set(newCapacity);
if (oldCapacity == null && newCapacity != null) {
inflateWithEmptyRecords(newCapacity);
if (!newCapacity.isSimEmpty()){
mIsCacheInvalidated.set(true);
fillCacheWithoutWaiting();
} else if (newCapacity.isSimValid()) {
notifyAdnLoadingWaiters();
tryFireUpdatePendingList();
} else {
notifyAdnLoadingWaiters();
logd("ADN capacity is invalid");
}
mIsInitialized.set(true); // Let's say the whole process is ready
} else {
// There is nothing from PB, so notify waiters directly if any
if (newCapacity.isSimValid() && newCapacity.isSimEmpty()) {
mIsCacheInvalidated.set(false);
notifyAdnLoadingWaiters();
tryFireUpdatePendingList();
} else if (!newCapacity.isSimValid()) {
mIsCacheInvalidated.set(false);
notifyAdnLoadingWaiters();
} else if (!mIsUpdateDone && !newCapacity.isSimEmpty()) {
invalidateSimPbCache();
fillCacheWithoutWaiting();
}
mIsUpdateDone = false;
}
}
private void inflateWithEmptyRecords(AdnCapacity capacity) {
if (ENABLE_INFLATE_WITH_EMPTY_RECORDS) {
logd("inflateWithEmptyRecords");
if (capacity != null && mSimPbRecords.isEmpty()) {
for (int i = 1; i <= capacity.getMaxAdnCount(); i++) {
mSimPbRecords.putIfAbsent(i,
new AdnRecord(IccConstants.EF_ADN, i, null, null, null, null));
}
}
}
}
private void handlePhonebookRecordReceived(ReceivedPhonebookRecords records) {
if (records != null) {
if (records.isOk()) {
logd("Partial data is received");
populateAdnRecords(records.getPhonebookRecords());
} else if (records.isCompleted()) {
logd("The whole loading process is finished");
populateAdnRecords(records.getPhonebookRecords());
mIsRecordLoading.set(false);
mIsInRetry.set(false);
mIsCacheInvalidated.set(false);
notifyAdnLoadingWaiters();
tryFireUpdatePendingList();
} else if (records.isRetryNeeded() && !mIsInRetry.get()) {
logd("Start to retry as aborted");
sendGettingPhonebookRecordsRetry(0);
} else {
loge("Error happened");
// Let's keep the stale data, in example of SIM getting removed during loading,
// expects to finish the whole process.
responseToWaitersWithErrorOrSuccess(true);
}
} else {
loge("No records there");
responseToWaitersWithErrorOrSuccess(true);
}
}
private void handleUpdatePhonebookRecordDone(AsyncResult ar) {
Exception e = null;
UpdateRequest updateRequest = (UpdateRequest)ar.userObj;
mIsUpdateDone = true;
if (ar.exception == null) {
int myRecordId = updateRequest.myRecordId;
AdnRecord adn = updateRequest.adnRecord;
int recordId = ((int[]) (ar.result))[0];
logd("my record ID = " + myRecordId + " new record ID = " + recordId);
if (myRecordId == INVALID_RECORD_ID || myRecordId == recordId) {
if (!adn.isEmpty()) {
addOrChangeSimPbRecord(adn, recordId);
} else {
deleteSimPbRecord(recordId);
}
} else {
e = new RuntimeException("The record ID for update doesn't match");
}
} else {
e = new RuntimeException("Update adn record failed", ar.exception);
}
if (mUpdateRequests.contains(updateRequest)) {
mUpdateRequests.remove(updateRequest);
updateRequest.responseResult(e);
} else {
loge("this update request isn't found");
}
tryFireUpdatePendingList();
}
private void tryFireUpdatePendingList() {
if (!mUpdateRequests.isEmpty()) {
updateSimPhonebook(mUpdateRequests.get(0));
}
}
private void handleSimRefresh(IccRefreshResponse iccRefreshResponse) {
if (iccRefreshResponse != null) {
if (iccRefreshResponse.refreshResult == IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE
&& (iccRefreshResponse.efId == IccConstants.EF_PBR ||
iccRefreshResponse.efId == IccConstants.EF_ADN) ||
iccRefreshResponse.refreshResult == IccRefreshResponse.REFRESH_RESULT_INIT) {
invalidateSimPbCache();
getSimPhonebookCapacity();
}
} else {
logd("IccRefreshResponse received is null");
}
}
private void populateAdnRecords(List<SimPhonebookRecord> records) {
if (records != null) {
Map<Integer, AdnRecord> newRecords = records.stream().map(record -> {return
new AdnRecord(IccConstants.EF_ADN,
record.getRecordId(),
record.getAlphaTag(),
record.getNumber(),
record.getEmails(),
record.getAdditionalNumbers());})
.collect(Collectors.toMap(AdnRecord::getRecId, adn -> adn));
mSimPbRecords.putAll(newRecords);
}
}
private void sendGettingPhonebookRecordsRetry (int times) {
if (hasMessages(EVENT_GET_PHONEBOOK_RECORDS_RETRY)) {
removeMessages(EVENT_GET_PHONEBOOK_RECORDS_RETRY);
}
mIsInRetry.set(true);
Message message = obtainMessage(EVENT_GET_PHONEBOOK_RECORDS_RETRY, 1, 0);
sendMessageDelayed(message, RETRY_INTERVAL);
}
private void addOrChangeSimPbRecord(AdnRecord record, int recordId) {
logd("Record number for the added or changed ADN is " + recordId);
record.setRecId(recordId);
if (ENABLE_INFLATE_WITH_EMPTY_RECORDS) {
mSimPbRecords.replace(recordId, record);
} else {
mSimPbRecords.put(recordId, record);
}
}
private void deleteSimPbRecord(int recordId) {
logd("Record number for the deleted ADN is " + recordId);
if (ENABLE_INFLATE_WITH_EMPTY_RECORDS) {
mSimPbRecords.replace(recordId,
new AdnRecord(IccConstants.EF_ADN, recordId, null, null, null, null));
} else {
if (mSimPbRecords.containsKey(recordId)) {
mSimPbRecords.remove(recordId);
}
}
}
private void invalidateSimPbCache() {
logd("invalidateSimPbCache");
mIsCacheInvalidated.set(true);
if (ENABLE_INFLATE_WITH_EMPTY_RECORDS) {
mSimPbRecords.replaceAll((k, v) ->
new AdnRecord(IccConstants.EF_ADN, k, null, null, null, null));
} else {
mSimPbRecords.clear();
}
}
private void logd(String msg) {
if (DBG) {
Rlog.d(LOG_TAG, msg);
}
}
private void loge(String msg) {
if (DBG) {
Rlog.e(LOG_TAG, msg);
}
}
private final static class UpdateRequest {
private int myRecordId;
private Message response;
private AdnRecord adnRecord;
private SimPhonebookRecord phonebookRecord;
UpdateRequest(int recordId, AdnRecord record, SimPhonebookRecord phonebookRecord,
Message response) {
this.myRecordId = recordId;
this.adnRecord = record;
this.phonebookRecord = phonebookRecord;
this.response = response;
}
void responseResult(Exception e) {
if (response != null) {
AsyncResult.forMessage(response, null, e);
response.sendToTarget();
}
}
}
}