1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.contacts.model;
17
18import android.accounts.AccountManager;
19import android.content.ContentResolver;
20import android.database.Cursor;
21import android.net.Uri;
22import android.provider.ContactsContract;
23import android.support.annotation.VisibleForTesting;
24
25import com.android.contacts.model.account.AccountWithDataSet;
26import com.android.contacts.util.DeviceLocalAccountTypeFactory;
27
28import java.util.ArrayList;
29import java.util.HashSet;
30import java.util.List;
31import java.util.Set;
32
33/**
34 * Attempts to create accounts for "Device" contacts by querying
35 * CP2 for records with {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE} columns
36 * that do not exist for any account returned by {@link AccountManager#getAccounts()}
37 *
38 * This class should be used from a background thread since it does DB queries
39 */
40public class Cp2DeviceLocalAccountLocator extends DeviceLocalAccountLocator {
41
42    // Note this class is assuming ACCOUNT_NAME and ACCOUNT_TYPE have same values in
43    // RawContacts, Groups, and Settings. This assumption simplifies the code somewhat and it
44    // is true right now and unlikely to ever change.
45    @VisibleForTesting
46    static String[] PROJECTION = new String[] {
47            ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE,
48            ContactsContract.RawContacts.DATA_SET
49    };
50
51    private static final int COL_NAME = 0;
52    private static final int COL_TYPE = 1;
53    private static final int COL_DATA_SET = 2;
54
55    private final ContentResolver mResolver;
56    private final DeviceLocalAccountTypeFactory mAccountTypeFactory;
57
58    private final String mSelection;
59    private final String[] mSelectionArgs;
60
61    public Cp2DeviceLocalAccountLocator(ContentResolver contentResolver,
62            DeviceLocalAccountTypeFactory factory,
63            Set<String> knownAccountTypes) {
64        mResolver = contentResolver;
65        mAccountTypeFactory = factory;
66
67        mSelection = getSelection(knownAccountTypes);
68        mSelectionArgs = getSelectionArgs(knownAccountTypes);
69    }
70
71    @Override
72    public List<AccountWithDataSet> getDeviceLocalAccounts() {
73
74        final Set<AccountWithDataSet> localAccounts = new HashSet<>();
75
76        // Many device accounts have default groups associated with them.
77        addAccountsFromQuery(ContactsContract.Groups.CONTENT_URI, localAccounts);
78        addAccountsFromQuery(ContactsContract.Settings.CONTENT_URI, localAccounts);
79        addAccountsFromQuery(ContactsContract.RawContacts.CONTENT_URI, localAccounts);
80
81        return new ArrayList<>(localAccounts);
82    }
83
84    private void addAccountsFromQuery(Uri uri, Set<AccountWithDataSet> accounts) {
85        final Cursor cursor = mResolver.query(uri, PROJECTION, mSelection, mSelectionArgs, null);
86
87        if (cursor == null) return;
88
89        try {
90            addAccountsFromCursor(cursor, accounts);
91        } finally {
92            cursor.close();
93        }
94    }
95
96    private void addAccountsFromCursor(Cursor cursor, Set<AccountWithDataSet> accounts) {
97        while (cursor.moveToNext()) {
98            final String name = cursor.getString(COL_NAME);
99            final String type = cursor.getString(COL_TYPE);
100            final String dataSet = cursor.getString(COL_DATA_SET);
101
102            if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
103                    mAccountTypeFactory, type)) {
104                accounts.add(new AccountWithDataSet(name, type, dataSet));
105            }
106        }
107    }
108
109    @VisibleForTesting
110    public String getSelection() {
111        return mSelection;
112    }
113
114    @VisibleForTesting
115    public String[] getSelectionArgs() {
116        return mSelectionArgs;
117    }
118
119    private static String getSelection(Set<String> knownAccountTypes) {
120        final StringBuilder sb = new StringBuilder()
121                .append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" IS NULL");
122        if (knownAccountTypes.isEmpty()) {
123            return sb.toString();
124        }
125        sb.append(" OR ").append(ContactsContract.RawContacts.ACCOUNT_TYPE).append(" NOT IN (");
126        for (String ignored : knownAccountTypes) {
127            sb.append("?,");
128        }
129        // Remove trailing ','
130        sb.deleteCharAt(sb.length() - 1).append(')');
131        return sb.toString();
132    }
133
134    private static String[] getSelectionArgs(Set<String> knownAccountTypes) {
135        if (knownAccountTypes.isEmpty()) return null;
136
137        return knownAccountTypes.toArray(new String[knownAccountTypes.size()]);
138    }
139}
140