blob: ca0011216af75cabeac8c513d2aef16bc8f5c456 [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 java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Iterator;
import java.util.List;
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 static final String LOG_TAG = "SimPhonebookRecordCache";
private static final boolean DBG = 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
// 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 List<AdnRecord> mSimPbRecords =
Collections.synchronizedList(new ArrayList<AdnRecord>());
private 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;
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) {
AsyncResult.forMessage(response).result = mSimPbRecords;
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();
boolean isCapacityInvalid = getAdnCapacity() == null;
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();
}
@VisibleForTesting
public boolean isLoading() {
return mIsRecordLoading.get();
}
@VisibleForTesting
public List<AdnRecord> getAdnRecords() {
return mSimPbRecords;
}
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 = false;
int index = 0;
for (Iterator<AdnRecord> it = mSimPbRecords.iterator(); it.hasNext();) {
AdnRecord oldAdn = it.next();
++index;
if (oldAdn.getRecId() == recordId) {
found = true;
break;
}
}
if (!found) {
sendErrorResponse(response, "There is an invalid old Adn for update");
return;
}
updateSimPhonebookByNewAdn(index, newAdn, response);
}
public void updateSimPbAdnBySearch(AdnRecord oldAdn, AdnRecord newAdn, Message response) {
int index = -1;
if ((oldAdn == null || oldAdn.isEmpty()) && !newAdn.isEmpty()) {
// Add contact
index = 0;
} else {
int count = 1;
// Delete or update contact
for (Iterator<AdnRecord> it = mSimPbRecords.iterator(); it.hasNext();) {
if (oldAdn.isEqual(it.next())) {
index = count;
break;
}
count++;
}
}
if (index == -1) {
sendErrorResponse(response, "SIM Phonebook record don't exist for " + oldAdn);
return;
}
if (newAdn == null) {
sendErrorResponse(response, "There is an invalid new Adn for update");
return;
}
if (index == 0 && mAdnCapacity.get() != null && mAdnCapacity.get().isSimFull()) {
sendErrorResponse(response, "SIM Phonebook record is full");
return;
}
updateSimPhonebookByNewAdn(index, newAdn, response);
}
private void updateSimPhonebookByNewAdn(int index, AdnRecord newAdn, Message response) {
int recordIndex = (index == 0) ? newAdn.getRecId()
: mSimPbRecords.get(index - 1).getRecId();
SimPhonebookRecord updateAdn = new SimPhonebookRecord.Builder()
.setRecordIndex(recordIndex)
.setAlphaTag(newAdn.getAlphaTag())
.setNumber(newAdn.getNumber())
.setEmails(newAdn.getEmails())
.setAdditionalNumbers(newAdn.getAdditionalNumbers())
.build();
UpdateRequest updateRequest = new UpdateRequest(index, newAdn, updateAdn, response);
mUpdateRequests.add(updateRequest);
boolean isCapacityInvalid = getAdnCapacity() == null;
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);
}
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();
mAdnCapacity.set(newCapacity);
if (oldCapacity == null && newCapacity != null) {
if (newCapacity.getMaxAdnCount() > 0){
mSimPbRecords.clear();
fillCacheWithoutWaiting();
} else {
notifyAdnLoadingWaiters();
}
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 != null && newCapacity.getMaxAdnCount() == 0) {
notifyAdnLoadingWaiters();
} else if (!mIsUpdateDone) {
invalidateSimPbCache();
fillCacheWithoutWaiting();
}
mIsUpdateDone = false;
}
}
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);
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 index = updateRequest.index;
AdnRecord adn = updateRequest.adnRecord;
int recordIndex = ((int[]) (ar.result))[0];
if (index == 0) {
// add contact
addSimPbRecord(adn, recordIndex);
} else if (adn.isEmpty()){
// delete contact
AdnRecord deletedRecord = mSimPbRecords.get(index - 1);
int adnRecordIndex = deletedRecord.getRecId();
logd("Record number for deleted ADN is " + adnRecordIndex);
if(recordIndex == adnRecordIndex) {
deleteSimPbRecord(index);
} else {
e = new RuntimeException(
"The index for deleted ADN record did not match");
}
} else {
// Change contact
if (mSimPbRecords.size() > index - 1) {
AdnRecord oldRecord = mSimPbRecords.get(index - 1);
int adnRecordIndex = oldRecord.getRecId();
logd("Record number for changed ADN is " + adnRecordIndex);
if(recordIndex == adnRecordIndex) {
updateSimPbRecord(adn, recordIndex, index);
} else {
e = new RuntimeException(
"The index for changed ADN record did not match");
}
} else {
e = new RuntimeException(
"The index for changed ADN record is out of the border");
}
}
} 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) {
List<AdnRecord> newRecords = records.stream().map(record -> {return
new AdnRecord(0, // PBR or ADN
record.getRecordIndex(),
record.getAlphaTag(),
record.getNumber(),
record.getEmails(),
record.getAdditionalNumbers());}).collect(Collectors.toList());
mSimPbRecords.addAll(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 addSimPbRecord(AdnRecord addedRecord, int recordIndex) {
logd("Record number for the added ADN is " + recordIndex);
addedRecord.setRecId(recordIndex);
mSimPbRecords.add(addedRecord);
}
private void deleteSimPbRecord(int index) {
logd("Record number for the deleted ADN is " + index);
mSimPbRecords.remove(index - 1);
}
private void updateSimPbRecord(AdnRecord newRecord,
int recordIndex, int index) {
logd("Record number for the updated ADN is " + recordIndex);
newRecord.setRecId(recordIndex);
mSimPbRecords.set(index - 1, newRecord);
}
private void invalidateSimPbCache() {
logd("invalidateSimPbCache");
mIsCacheInvalidated.set(true);
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 index;
private Message response;
private AdnRecord adnRecord;
private SimPhonebookRecord phonebookRecord;
UpdateRequest(int index, AdnRecord record, SimPhonebookRecord phonebookRecord,
Message response) {
this.index = index;
this.adnRecord = record;
this.phonebookRecord = phonebookRecord;
this.response = response;
}
void responseResult(Exception e) {
if (response != null) {
AsyncResult.forMessage(response, null, e);
response.sendToTarget();
}
}
}
}