Add ability to detect that SIM import is in progress
SIM import might be long running so things may need to be updated
when it starts rather then when it completes.
Test
See Iae69fe2d96bae6d268f77515ca85eb572c2dfe9b
Bug 31781331
Change-Id: I9efdb181b97f3fdf49cd31ba5d3eae43bcad674c
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 853e676..d2a65a8 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -49,6 +49,7 @@
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.os.ResultReceiver;
+import android.telephony.SubscriptionInfo;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
@@ -62,6 +63,7 @@
import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.RawContactDeltaList;
import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.SimCard;
import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.preference.ContactsPreferences;
@@ -70,11 +72,11 @@
import com.android.contacts.compat.PinnedPositionsCompat;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contactsbind.FeedbackHelper;
-
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -149,10 +151,16 @@
public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
public static final String EXTRA_SIM_CONTACTS = "simContacts";
+ public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
+
+ // For debugging and testing what happens when requests are queued up.
+ public static final String ACTION_SLEEP = "sleep";
+ public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
- public static final String EXTRA_CALLBACK_DATA = "extraCallbackData";
+
+ public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
public static final String EXTRA_RESULT_CODE = "resultCode";
public static final String EXTRA_RESULT_COUNT = "count";
@@ -197,6 +205,9 @@
private static final CopyOnWriteArrayList<Listener> sListeners =
new CopyOnWriteArrayList<Listener>();
+ // Holds the current state of the service
+ private static final State sState = new State();
+
private Handler mMainHandler;
private GroupsDao mGroupsDao;
private SimContactDao mSimContactDao;
@@ -230,6 +241,15 @@
sListeners.remove(listener);
}
+ public static State getState() {
+ return sState;
+ }
+
+ private void notifyStateChanged() {
+ LocalBroadcastManager.getInstance(this)
+ .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
+ }
+
/**
* Returns true if the ContactSaveService was started successfully and false if an exception
* was thrown and a Toast error message was displayed.
@@ -279,8 +299,17 @@
return getApplicationContext().getSystemService(name);
}
+ // Parent classes Javadoc says not to override this method but we're doing it just to update
+ // our state which should be OK since we're still doing the work in onHandleIntent
@Override
- protected void onHandleIntent(Intent intent) {
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ sState.onStart(intent);
+ notifyStateChanged();
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ protected void onHandleIntent(final Intent intent) {
if (intent == null) {
Log.d(TAG, "onHandleIntent: could not handle null intent");
return;
@@ -332,7 +361,12 @@
undo(intent);
} else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
importFromSim(intent);
+ } else if (ACTION_SLEEP.equals(action)) {
+ sleepForDebugging(intent);
}
+
+ sState.onFinish(intent);
+ notifyStateChanged();
}
/**
@@ -1688,36 +1722,82 @@
operations.add(builder.build());
}
- public static Intent createImportFromSimIntent(@NonNull Context context,
- @NonNull ArrayList<SimContact> contacts, @NonNull AccountWithDataSet targetAccount,
- @Nullable Bundle callbackData) {
+ /**
+ * Returns an intent that can be used to import the contacts into targetAccount.
+ *
+ * @param context context to use for creating the intent
+ * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
+ * {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
+ * SIM for that subscription ID will be marked as imported
+ * @param contacts the contacts to import
+ * @param targetAccount the account import the contacts into
+ */
+ public static Intent createImportFromSimIntent(Context context, int subscriptionId,
+ ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
return new Intent(context, ContactSaveService.class)
.setAction(ACTION_IMPORT_FROM_SIM)
.putExtra(EXTRA_SIM_CONTACTS, contacts)
- .putExtra(EXTRA_ACCOUNT, targetAccount)
- .putExtra(EXTRA_CALLBACK_DATA, callbackData);
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
+ .putExtra(EXTRA_ACCOUNT, targetAccount);
}
private void importFromSim(Intent intent) {
final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
.putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
+ final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
+ SimCard.NO_SUBSCRIPTION_ID);
try {
final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
final ArrayList<SimContact> contacts =
intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
mSimContactDao.importContacts(contacts, targetAccount);
+
+ // Update the imported state of the SIM card that was imported
+ final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
+ if (sim != null) {
+ mSimContactDao.persistSimState(sim.withImportedState(true));
+ }
+
// notify success
LocalBroadcastManager.getInstance(this).sendBroadcast(result
.putExtra(EXTRA_RESULT_COUNT, contacts.size())
.putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
- .putExtra(EXTRA_CALLBACK_DATA, intent.getBundleExtra(EXTRA_CALLBACK_DATA)));
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "importFromSim completed successfully");
}
} catch (RemoteException|OperationApplicationException e) {
FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
LocalBroadcastManager.getInstance(this).sendBroadcast(result
- .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
+ .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
+ }
+ }
+
+ /**
+ * Returns an intent that can start this service and cause it to sleep for the specified time.
+ *
+ * This exists purely for debugging and manual testing. Since this service uses a single thread
+ * it is useful to have a way to test behavior when work is queued up and most of the other
+ * operations complete too quickly to simulate that under normal conditions.
+ */
+ public static Intent createSleepIntent(Context context, long millis) {
+ return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
+ .putExtra(EXTRA_SLEEP_DURATION, millis);
+ }
+
+ private void sleepForDebugging(Intent intent) {
+ long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sleeping for " + duration + "ms");
+ }
+ try {
+ Thread.sleep(duration);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "finished sleeping");
}
}
@@ -1874,4 +1954,62 @@
return contentResolver.delete(groupUri, null, null);
}
}
+
+ /**
+ * Keeps track of which operations have been requested but have not yet finished for this
+ * service.
+ */
+ public static class State {
+ private final CopyOnWriteArrayList<Intent> mPending;
+
+ public State() {
+ mPending = new CopyOnWriteArrayList<>();
+ }
+
+ public State(Collection<Intent> pendingActions) {
+ mPending = new CopyOnWriteArrayList<>(pendingActions);
+ }
+
+ public boolean isIdle() {
+ return mPending.isEmpty();
+ }
+
+ public Intent getCurrentIntent() {
+ return mPending.isEmpty() ? null : mPending.get(0);
+ }
+
+ /**
+ * Returns the first intent requested that has the specified action or null if no intent
+ * with that action has been requested.
+ */
+ public Intent getNextIntentWithAction(String action) {
+ for (Intent intent : mPending) {
+ if (action.equals(intent.getAction())) {
+ return intent;
+ }
+ }
+ return null;
+ }
+
+ public boolean isActionPending(String action) {
+ return getNextIntentWithAction(action) != null;
+ }
+
+ private void onFinish(Intent intent) {
+ if (mPending.isEmpty()) {
+ return;
+ }
+ final String action = mPending.get(0).getAction();
+ if (action.equals(intent.getAction())) {
+ mPending.remove(0);
+ }
+ }
+
+ private void onStart(Intent intent) {
+ if (intent.getAction() == null) {
+ return;
+ }
+ mPending.add(intent);
+ }
+ }
}
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index 2bd4bac..d9030a8 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -65,8 +65,6 @@
private static final String KEY_SELECTED_IDS = "selectedIds";
private static final String ARG_SUBSCRIPTION_ID = "subscriptionId";
- public static final String CALLBACK_KEY_SUBSCRIPTION_ID = "simSubscriptionId";
-
private ContactsPreferences mPreferences;
private AccountTypeManager mAccountTypeManager;
private SimContactAdapter mAdapter;
@@ -221,11 +219,10 @@
}
private void importCurrentSelections() {
- final Bundle callbackData = new Bundle();
- callbackData.putInt(CALLBACK_KEY_SUBSCRIPTION_ID, mSubscriptionId);
ContactSaveService.startService(getContext(), ContactSaveService
- .createImportFromSimIntent(getContext(), mAdapter.getSelectedContacts(),
- mAccountHeaderPresenter.getCurrentAccount(), callbackData));
+ .createImportFromSimIntent(getContext(), mSubscriptionId,
+ mAdapter.getSelectedContacts(),
+ mAccountHeaderPresenter.getCurrentAccount()));
}
@Override
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index ee36645..4ea5c02 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -71,6 +71,10 @@
// to work on any phone.
private static final int IMPORT_MAX_BATCH_SIZE = 300;
+ // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+ // query parameter limit.
+ static final int QUERY_MAX_BATCH_SIZE = 100;
+
// Set to true for manual testing on an emulator or phone without a SIM card
// DO NOT SUBMIT if set to true
private static final boolean USE_FAKE_INSTANCE = false;
@@ -198,9 +202,9 @@
public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
List<SimContact> contacts) {
final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
- for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
+ for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
findAccountsOfExistingSimContacts(
- contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
+ contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
result);
}
return result;
@@ -383,16 +387,16 @@
public static SimContactDao create(Context context) {
if (USE_FAKE_INSTANCE) {
return new DebugImpl(context)
- .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier", "Card 1",
- "15095550101", "us").withContacts(
+ .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier",
+ "Card 1", "15095550101", "us").withContacts(
new SimContact(1, "Sim One", "15095550111", null),
new SimContact(2, "Sim Two", "15095550112", null),
new SimContact(3, "Sim Three", "15095550113", null),
new SimContact(4, "Sim Four", "15095550114", null),
new SimContact(5, "411 & more", "411", null)
))
- .addSimCard(new SimCard("fake-sim-id2", 2, "Carrier Two", "Card 2",
- "15095550102", "us").withContacts(
+ .addSimCard(new SimCard("fake-sim-id2", 2, "Carrier Two",
+ "Card 2", "15095550102", "us").withContacts(
new SimContact(1, "John Sim", "15095550121", null),
new SimContact(2, "Bob Sim", "15095550122", null),
new SimContact(3, "Mary Sim", "15095550123", null),
@@ -405,7 +409,7 @@
// TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
// active development or anytime after 3/1/2017
- private static class DebugImpl extends SimContactDao {
+ public static class DebugImpl extends SimContactDao {
private List<SimCard> mSimCards = new ArrayList<>();
private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
@@ -447,9 +451,6 @@
// Query used for detecting existing contacts that may match a SimContact.
private static final class DataQuery {
- // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
- // query parameter limit.
- static final int MAX_BATCH_SIZE = 100;
public static final String[] PROJECTION = new String[] {
ContactsContract.Data.RAW_CONTACT_ID, Phone.NUMBER, Phone.DISPLAY_NAME