Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2bb4984e authored by Marcus Hagerott's avatar Marcus Hagerott
Browse files

DO NOT MERGE Improve testability of SIM import code.

For Iec6eb441fe197daceb24e87288f8e0c5ac0ce2cf

test
ran GoogleContactsTests

Change-Id: I7d34e68f389143f94c26190cd9b3206ca871c64e
(cherry picked from commit 52d8cae888457d9639696a19db29a2ff36deca85)
parent 95246bb8
Loading
Loading
Loading
Loading
+36 −8
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.util.TimingLogger;

import com.android.contacts.activities.PeopleActivity;
import com.android.contacts.common.database.SimContactDao;
import com.android.contacts.common.database.SimContactDaoImpl;
import com.android.contacts.common.model.SimCard;
import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
@@ -49,6 +50,26 @@ public class SimImportService extends Service {

    private static final String TAG = "SimImportService";

    /**
     * Wrapper around the service state for testability
     */
    public interface StatusProvider {

        /**
         * Returns whether there is any imports still pending
         *
         * <p>This should be called from the UI thread</p>
         */
        boolean isRunning();

        /**
         * Returns whether an import for sim has been requested
         *
         * <p>This should be called from the UI thread</p>
         */
        boolean isImporting(SimCard sim);
    }

    public static final String EXTRA_ACCOUNT = "account";
    public static final String EXTRA_SIM_CONTACTS = "simContacts";
    public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
@@ -74,12 +95,24 @@ public class SimImportService extends Service {
    // Keeps track of current tasks. This is only modified from the UI thread.
    private static List<ImportTask> sPending = new ArrayList<>();

    private static StatusProvider sStatusProvider = new StatusProvider() {
        @Override
        public boolean isRunning() {
            return !sPending.isEmpty();
        }

        @Override
        public boolean isImporting(SimCard sim) {
            return SimImportService.isImporting(sim);
        }
    };

    /**
     * Returns whether an import for sim has been requested
     *
     * <p>This should be called from the UI thread</p>
     */
    public static boolean isImporting(SimCard sim) {
    private static boolean isImporting(SimCard sim) {
        for (ImportTask task : sPending) {
            if (task.getSim().equals(sim)) {
                return true;
@@ -88,13 +121,8 @@ public class SimImportService extends Service {
        return false;
    }

    /**
     * Returns whether there is any imports still pending
     *
     * <p>This should be called from the UI thread</p>
     */
    public static boolean isRunning() {
        return !sPending.isEmpty();
    public static StatusProvider getStatusProvider() {
        return sStatusProvider;
    }

    /**
+15 −438
Original line number Diff line number Diff line
@@ -15,46 +15,19 @@
 */
package com.android.contacts.common.database;

import android.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.ArraySet;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.SparseArray;

import com.android.contacts.R;
import com.android.contacts.common.compat.CompatUtils;
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.util.PermissionsUtil;
import com.android.contacts.util.SharedPreferenceUtil;
import com.google.common.base.Joiner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -63,349 +36,15 @@ import java.util.Set;
 * Provides data access methods for loading contacts from a SIM card and and migrating these
 * SIM contacts to a CP2 account.
 */
public class SimContactDao {
    private static final String TAG = "SimContactDao";

    // Maximum number of SIM contacts to import in a single ContentResolver.applyBatch call.
    // This is necessary to avoid TransactionTooLargeException when there are a large number of
    // contacts. This has been tested on Nexus 6 NME70B and is probably be conservative enough
    // 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;
public abstract class SimContactDao {

    // 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;

    @VisibleForTesting
    public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");

    public static String _ID = BaseColumns._ID;
    public static String NAME = "name";
    public static String NUMBER = "number";
    public static String EMAILS = "emails";

    private final Context mContext;
    private final ContentResolver mResolver;
    private final TelephonyManager mTelephonyManager;

    private SimContactDao(Context context) {
        mContext = context;
        mResolver = context.getContentResolver();
        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    }


    public Context getContext() {
        return mContext;
    }

    public boolean canReadSimContacts() {
        // Require SIM_STATE_READY because the TelephonyManager methods related to SIM require
        // this state
        return hasTelephony() && hasPermissions() &&
                mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY;
    }

    public List<SimCard> getSimCards() {
        if (!canReadSimContacts()) {
            return Collections.emptyList();
        }
        final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
                getSimCardsFromSubscriptions() :
                Collections.singletonList(SimCard.create(mTelephonyManager,
                        mContext.getString(R.string.single_sim_display_label)));
        return SharedPreferenceUtil.restoreSimStates(mContext, sims);
    }

    public List<SimCard> getSimCardsWithContacts() {
        final List<SimCard> result = new ArrayList<>();
        for (SimCard sim : getSimCards()) {
            result.add(sim.withContacts(loadContactsForSim(sim)));
        }
        return result;
    }

    public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
        if (sim.hasValidSubscriptionId()) {
            return loadSimContacts(sim.getSubscriptionId());
        }
        return loadSimContacts();
    }

    public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
        return loadFrom(ICC_CONTENT_URI.buildUpon()
                .appendPath("subId")
                .appendPath(String.valueOf(subscriptionId))
                .build());
    }

    public ArrayList<SimContact> loadSimContacts() {
        return loadFrom(ICC_CONTENT_URI);
    }

    public ContentProviderResult[] importContacts(List<SimContact> contacts,
            AccountWithDataSet targetAccount)
            throws RemoteException, OperationApplicationException {
        if (contacts.size() < IMPORT_MAX_BATCH_SIZE) {
            return importBatch(contacts, targetAccount);
        }
        final List<ContentProviderResult> results = new ArrayList<>();
        for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
            results.addAll(Arrays.asList(importBatch(
                    contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
                    targetAccount)));
        }
        return results.toArray(new ContentProviderResult[results.size()]);
    }

    public void persistSimState(SimCard sim) {
        SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
    }

    public void persistSimStates(List<SimCard> simCards) {
        SharedPreferenceUtil.persistSimStates(mContext, simCards);
    }

    public SimCard getFirstSimCard() {
        return getSimBySubscriptionId(SimCard.NO_SUBSCRIPTION_ID);
    }

    public SimCard getSimBySubscriptionId(int subscriptionId) {
        final List<SimCard> sims = SharedPreferenceUtil.restoreSimStates(mContext, getSimCards());
        if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
            return sims.get(0);
        }
        for (SimCard sim : getSimCards()) {
            if (sim.getSubscriptionId() == subscriptionId) {
                return sim;
            }
        }
        return null;
    }

    /**
     * Finds SIM contacts that exist in CP2 and associates the account of the CP2 contact with
     * the SIM contact
     */
    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 += QUERY_MAX_BATCH_SIZE) {
            findAccountsOfExistingSimContacts(
                    contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
                    result);
        }
        return result;
    }

    private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
            Map<AccountWithDataSet, Set<SimContact>> result) {
        final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
        Collections.sort(contacts, SimContact.compareByPhoneThenName());

        final Cursor dataCursor = queryRawContactsForSimContacts(contacts);

        try {
            while (dataCursor.moveToNext()) {
                final String number = DataQuery.getPhoneNumber(dataCursor);
                final String name = DataQuery.getDisplayName(dataCursor);

                final int index = SimContact.findByPhoneAndName(contacts, number, name);
                if (index < 0) {
                    continue;
                }
                final SimContact contact = contacts.get(index);
                final long id = DataQuery.getRawContactId(dataCursor);
                if (!rawContactToSimContact.containsKey(id)) {
                    rawContactToSimContact.put(id, new ArrayList<SimContact>());
                }
                rawContactToSimContact.get(id).add(contact);
            }
        } finally {
            dataCursor.close();
        }

        final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
        try {
            while (accountsCursor.moveToNext()) {
                final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
                final long id = AccountQuery.getId(accountsCursor);
                if (!result.containsKey(account)) {
                    result.put(account, new ArraySet<SimContact>());
                }
                for (SimContact contact : rawContactToSimContact.get(id)) {
                    result.get(account).add(contact);
                }
            }
        } finally {
            accountsCursor.close();
        }
    }

    private ContentProviderResult[] importBatch(List<SimContact> contacts,
            AccountWithDataSet targetAccount)
            throws RemoteException, OperationApplicationException {
        final ArrayList<ContentProviderOperation> ops =
                createImportOperations(contacts, targetAccount);
        return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
    private List<SimCard> getSimCardsFromSubscriptions() {
        final SubscriptionManager subscriptionManager = (SubscriptionManager)
                mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
        final List<SubscriptionInfo> subscriptions = subscriptionManager
                .getActiveSubscriptionInfoList();
        final ArrayList<SimCard> result = new ArrayList<>();
        for (SubscriptionInfo subscriptionInfo : subscriptions) {
            result.add(SimCard.create(subscriptionInfo));
        }
        return result;
    }

    private List<SimContact> getContactsForSim(SimCard sim) {
        final List<SimContact> contacts = sim.getContacts();
        return contacts != null ? contacts : loadContactsForSim(sim);
    }

    // See b/32831092
    // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
    // concurrently. So we just have a global lock around it to prevent potential issues.
    private static final Object SIM_READ_LOCK = new Object();
    private ArrayList<SimContact> loadFrom(Uri uri) {
        synchronized (SIM_READ_LOCK) {
            final Cursor cursor = mResolver.query(uri, null, null, null, null);

            try {
                return loadFromCursor(cursor);
            } finally {
                cursor.close();
            }
        }
    }

    private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
        final int colId = cursor.getColumnIndex(_ID);
        final int colName = cursor.getColumnIndex(NAME);
        final int colNumber = cursor.getColumnIndex(NUMBER);
        final int colEmails = cursor.getColumnIndex(EMAILS);

        final ArrayList<SimContact> result = new ArrayList<>();

        while (cursor.moveToNext()) {
            final long id = cursor.getLong(colId);
            final String name = cursor.getString(colName);
            final String number = cursor.getString(colNumber);
            final String emails = cursor.getString(colEmails);

            final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
            result.add(contact);
        }
        return result;
    }

    private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
        final StringBuilder selectionBuilder = new StringBuilder();

        int phoneCount = 0;
        int nameCount = 0;
        for (SimContact contact : contacts) {
            if (contact.hasPhone()) {
                phoneCount++;
            } else if (contact.hasName()) {
                nameCount++;
            }
        }
        List<String> selectionArgs = new ArrayList<>(phoneCount + 1);

        selectionBuilder.append('(');
        selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
        selectionArgs.add(Phone.CONTENT_ITEM_TYPE);

        selectionBuilder.append(Phone.NUMBER).append(" IN (")
                .append(Joiner.on(',').join(Collections.nCopies(phoneCount, '?')))
                .append(')');
        for (SimContact contact : contacts) {
            if (contact.hasPhone()) {
                selectionArgs.add(contact.getPhone());
            }
        }
        selectionBuilder.append(')');

        if (nameCount > 0) {
            selectionBuilder.append(" OR (");

            selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
            selectionArgs.add(StructuredName.CONTENT_ITEM_TYPE);

            selectionBuilder.append(Data.DISPLAY_NAME).append(" IN (")
                    .append(Joiner.on(',').join(Collections.nCopies(nameCount, '?')))
                    .append(')');
            for (SimContact contact : contacts) {
                if (!contact.hasPhone() && contact.hasName()) {
                    selectionArgs.add(contact.getName());
                }
            }
            selectionBuilder.append(')');
        }

        return mResolver.query(Data.CONTENT_URI.buildUpon()
                        .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
                        .build(),
                DataQuery.PROJECTION,
                selectionBuilder.toString(),
                selectionArgs.toArray(new String[selectionArgs.size()]),
                null);
    }

    private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
        final StringBuilder selectionBuilder = new StringBuilder();

        final String[] args = new String[ids.size()];

        selectionBuilder.append(RawContacts._ID).append(" IN (")
                .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
                .append(")");
        int i = 0;
        for (long id : ids) {
            args[i++] = String.valueOf(id);
        }
        return mResolver.query(RawContacts.CONTENT_URI,
                AccountQuery.PROJECTION,
                selectionBuilder.toString(),
                args,
                null);
    }

    private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
            AccountWithDataSet targetAccount) {
        final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
        for (SimContact contact : contacts) {
            contact.appendCreateContactOperations(ops, targetAccount);
        }
        return ops;
    }

    private String[] parseEmails(String emails) {
        return emails != null ? emails.split(",") : null;
    }

    private boolean hasTelephony() {
        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
    }

    private boolean hasPermissions() {
        return PermissionsUtil.hasContactsPermissions(mContext) &&
                PermissionsUtil.hasPhonePermissions(mContext);
    }

    public static SimContactDao create(Context context) {
        if (USE_FAKE_INSTANCE) {
            return new DebugImpl(context)
            return new SimContactDaoImpl.DebugImpl(context)
                    .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier",
                            "Card 1", "15095550101", "us").withContacts(
                            new SimContact(1, "Sim One", "15095550111", null),
@@ -423,89 +62,27 @@ public class SimContactDao {
                            new SimContact(5, "Sim Duplicate", "15095550121", null)
                    ));
        }
        return new SimContactDao(context);
    }

    // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
    // active development or anytime after 3/1/2017
    public static class DebugImpl extends SimContactDao {

        private List<SimCard> mSimCards = new ArrayList<>();
        private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();

        public DebugImpl(Context context) {
            super(context);
        }

        public DebugImpl addSimCard(SimCard sim) {
            mSimCards.add(sim);
            mCardsBySubscription.put(sim.getSubscriptionId(), sim);
            return this;
        }

        @Override
        public List<SimCard> getSimCards() {
            return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
        }

        @Override
        public ArrayList<SimContact> loadSimContacts() {
            return new ArrayList<>(mSimCards.get(0).getContacts());
        }

        @Override
        public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
            return new ArrayList<>(mCardsBySubscription.get(subscriptionId).getContacts());
        return new SimContactDaoImpl(context);
    }

        @Override
        public boolean canReadSimContacts() {
            return true;
        }
    }
    public abstract boolean canReadSimContacts();

    // Query used for detecting existing contacts that may match a SimContact.
    private static final class DataQuery {
    public abstract List<SimCard> getSimCards();

        public static final String[] PROJECTION = new String[] {
                Data.RAW_CONTACT_ID, Phone.NUMBER, Data.DISPLAY_NAME, Data.MIMETYPE
        };
    public abstract ArrayList<SimContact> loadContactsForSim(SimCard sim);

        public static final int RAW_CONTACT_ID = 0;
        public static final int PHONE_NUMBER = 1;
        public static final int DISPLAY_NAME = 2;
        public static final int MIMETYPE = 3;

        public static long getRawContactId(Cursor cursor) {
            return cursor.getLong(RAW_CONTACT_ID);
        }

        public static String getPhoneNumber(Cursor cursor) {
            return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
        }

        public static String getDisplayName(Cursor cursor) {
            return cursor.getString(DISPLAY_NAME);
        }
    public abstract ContentProviderResult[] importContacts(List<SimContact> contacts,
            AccountWithDataSet targetAccount)
            throws RemoteException, OperationApplicationException;

        public static boolean isPhoneNumber(Cursor cursor) {
            return Phone.CONTENT_ITEM_TYPE.equals(cursor.getString(MIMETYPE));
        }
    }
    public abstract void persistSimStates(List<SimCard> simCards);

    private static final class AccountQuery {
        public static final String[] PROJECTION = new String[] {
                RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
                RawContacts.DATA_SET
        };
    public abstract SimCard getSimBySubscriptionId(int subscriptionId);

        public static long getId(Cursor cursor) {
            return cursor.getLong(0);
        }
    public abstract Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
            List<SimContact> contacts);

        public static AccountWithDataSet getAccount(Cursor cursor) {
            return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
                    cursor.getString(3));
        }
    public void persistSimState(SimCard sim) {
        persistSimStates(Collections.singletonList(sim));
    }
}
+473 −0

File added.

Preview size limit exceeded, changes collapsed.

+14 −0
Original line number Diff line number Diff line
@@ -221,6 +221,20 @@ public class SimCard {
        return result;
    }

    @Override
    public String toString() {
        return "SimCard{" +
                "mSimId='" + mSimId + '\'' +
                ", mSubscriptionId=" + mSubscriptionId +
                ", mCarrierName=" + mCarrierName +
                ", mDisplayName=" + mDisplayName +
                ", mPhoneNumber='" + mPhoneNumber + '\'' +
                ", mCountryCode='" + mCountryCode + '\'' +
                ", mDismissed=" + mDismissed +
                ", mImported=" + mImported +
                ", mContacts=" + mContacts +
                '}';
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
    public static SimCard create(SubscriptionInfo info) {
+3 −1
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.support.test.filters.SdkSuppress;
import android.support.test.filters.Suppress;
import android.support.test.runner.AndroidJUnit4;

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.tests.AccountsTestHelper;
@@ -281,7 +282,8 @@ public class SimContactDaoTests {
            mSimTestHelper.addSimContact("Test Simthree", "15095550103");

            final SimContactDao sut = SimContactDao.create(getContext());
            final ArrayList<SimContact> contacts = sut.loadSimContacts();
            final SimCard sim = sut.getSimCards().get(0);
            final ArrayList<SimContact> contacts = sut.loadContactsForSim(sim);

            assertThat(contacts.get(0), isSimContactWithNameAndPhone("Test Simone", "15095550101"));
            assertThat(contacts.get(1), isSimContactWithNameAndPhone("Test Simtwo", "15095550102"));
Loading