/* * 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. *

* This will be {@link #INVALID} if either is the case: *

    *
  1. The device does not support * {@link android.telephony.TelephonyManager#CAPABILITY_SIM_PHONEBOOK_IN_MODEM}.
  2. *
* {@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 mAdnCapacity = new AtomicReference(null); private Object mReadLock = new Object(); private final ConcurrentSkipListMap mSimPbRecords = new ConcurrentSkipListMap(); private final List mUpdateRequests = Collections.synchronizedList(new ArrayList()); // 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 mAdnLoadingWaiters = new ArrayList(); /** * 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 result = new ArrayList(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 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 { 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 (!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 records) { if (records != null) { Map 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(); } } } }