1/*
2 * Copyright (C) 2009 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 */
16
17package com.android.loaderapp.model;
18
19import com.android.loaderapp.model.ContactsSource.DataKind;
20import com.google.android.collect.Lists;
21import com.google.android.collect.Maps;
22import com.google.android.collect.Sets;
23
24import android.accounts.Account;
25import android.accounts.AccountManager;
26import android.accounts.AuthenticatorDescription;
27import android.accounts.OnAccountsUpdateListener;
28import android.content.BroadcastReceiver;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.IContentService;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.SyncAdapterType;
35import android.content.pm.PackageManager;
36import android.os.RemoteException;
37import android.provider.ContactsContract;
38import android.text.TextUtils;
39import android.util.Log;
40
41import java.lang.ref.SoftReference;
42import java.util.ArrayList;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.Locale;
46
47/**
48 * Singleton holder for all parsed {@link ContactsSource} available on the
49 * system, typically filled through {@link PackageManager} queries.
50 */
51public class Sources extends BroadcastReceiver implements OnAccountsUpdateListener {
52    private static final String TAG = "Sources";
53
54    private Context mContext;
55    private Context mApplicationContext;
56    private AccountManager mAccountManager;
57
58    private ContactsSource mFallbackSource = null;
59
60    private HashMap<String, ContactsSource> mSources = Maps.newHashMap();
61    private HashSet<String> mKnownPackages = Sets.newHashSet();
62
63    private static SoftReference<Sources> sInstance = null;
64
65    /**
66     * Requests the singleton instance of {@link Sources} with data bound from
67     * the available authenticators. This method blocks until its interaction
68     * with {@link AccountManager} is finished, so don't call from a UI thread.
69     */
70    public static synchronized Sources getInstance(Context context) {
71        Sources sources = sInstance == null ? null : sInstance.get();
72        if (sources == null) {
73            sources = new Sources(context);
74            sInstance = new SoftReference<Sources>(sources);
75        }
76        return sources;
77    }
78
79    /**
80     * Internal constructor that only performs initial parsing.
81     */
82    private Sources(Context context) {
83        mContext = context;
84        mApplicationContext = context.getApplicationContext();
85        mAccountManager = AccountManager.get(mApplicationContext);
86
87        // Create fallback contacts source for on-phone contacts
88        mFallbackSource = new FallbackSource();
89
90        queryAccounts();
91
92        // Request updates when packages or accounts change
93        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
94        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
95        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
96        filter.addDataScheme("package");
97        mApplicationContext.registerReceiver(this, filter);
98        IntentFilter sdFilter = new IntentFilter();
99        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
100        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
101        mApplicationContext.registerReceiver(this, sdFilter);
102
103        // Request updates when locale is changed so that the order of each field will
104        // be able to be changed on the locale change.
105        filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
106        mApplicationContext.registerReceiver(this, filter);
107
108        mAccountManager.addOnAccountsUpdatedListener(this, null, false);
109    }
110
111    /** @hide exposed for unit tests */
112    public Sources(ContactsSource... sources) {
113        for (ContactsSource source : sources) {
114            addSource(source);
115        }
116    }
117
118    protected void addSource(ContactsSource source) {
119        mSources.put(source.accountType, source);
120        mKnownPackages.add(source.resPackageName);
121    }
122
123    /** {@inheritDoc} */
124    @Override
125    public void onReceive(Context context, Intent intent) {
126        final String action = intent.getAction();
127
128        if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
129                || Intent.ACTION_PACKAGE_ADDED.equals(action)
130                || Intent.ACTION_PACKAGE_CHANGED.equals(action) ||
131                Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) ||
132                Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
133            String[] pkgList = null;
134            // Handle applications on sdcard.
135            if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) ||
136                    Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
137                pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
138            } else {
139                final String packageName = intent.getData().getSchemeSpecificPart();
140                pkgList = new String[] { packageName };
141            }
142            if (pkgList != null) {
143                for (String packageName : pkgList) {
144                    final boolean knownPackage = mKnownPackages.contains(packageName);
145                    if (knownPackage) {
146                        // Invalidate cache of existing source
147                        invalidateCache(packageName);
148                    } else {
149                        // Unknown source, so reload from scratch
150                        queryAccounts();
151                    }
152                }
153            }
154        } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
155            invalidateAllCache();
156        }
157    }
158
159    protected void invalidateCache(String packageName) {
160        for (ContactsSource source : mSources.values()) {
161            if (TextUtils.equals(packageName, source.resPackageName)) {
162                // Invalidate any cache for the changed package
163                source.invalidateCache();
164            }
165        }
166    }
167
168    protected void invalidateAllCache() {
169        mFallbackSource.invalidateCache();
170        for (ContactsSource source : mSources.values()) {
171            source.invalidateCache();
172        }
173    }
174
175    /** {@inheritDoc} */
176    public void onAccountsUpdated(Account[] accounts) {
177        // Refresh to catch any changed accounts
178        queryAccounts();
179    }
180
181    /**
182     * Blocking call to load all {@link AuthenticatorDescription} known by the
183     * {@link AccountManager} on the system.
184     */
185    protected synchronized void queryAccounts() {
186        mSources.clear();
187        mKnownPackages.clear();
188
189        final AccountManager am = mAccountManager;
190        final IContentService cs = ContentResolver.getContentService();
191
192        try {
193            final SyncAdapterType[] syncs = cs.getSyncAdapterTypes();
194            final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
195
196            for (SyncAdapterType sync : syncs) {
197                if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
198                    // Skip sync adapters that don't provide contact data.
199                    continue;
200                }
201
202                // Look for the formatting details provided by each sync
203                // adapter, using the authenticator to find general resources.
204                final String accountType = sync.accountType;
205                final AuthenticatorDescription auth = findAuthenticator(auths, accountType);
206
207                ContactsSource source;
208                if (GoogleSource.ACCOUNT_TYPE.equals(accountType)) {
209                    source = new GoogleSource(auth.packageName);
210                } else if (ExchangeSource.ACCOUNT_TYPE.equals(accountType)) {
211                    source = new ExchangeSource(auth.packageName);
212                } else {
213                    // TODO: use syncadapter package instead, since it provides resources
214                    Log.d(TAG, "Creating external source for type=" + accountType
215                            + ", packageName=" + auth.packageName);
216                    source = new ExternalSource(auth.packageName);
217                    source.readOnly = !sync.supportsUploading();
218                }
219
220                source.accountType = auth.type;
221                source.titleRes = auth.labelId;
222                source.iconRes = auth.iconId;
223
224                addSource(source);
225            }
226        } catch (RemoteException e) {
227            Log.w(TAG, "Problem loading accounts: " + e.toString());
228        }
229    }
230
231    /**
232     * Find a specific {@link AuthenticatorDescription} in the provided list
233     * that matches the given account type.
234     */
235    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
236            String accountType) {
237        for (AuthenticatorDescription auth : auths) {
238            if (accountType.equals(auth.type)) {
239                return auth;
240            }
241        }
242        throw new IllegalStateException("Couldn't find authenticator for specific account type");
243    }
244
245    /**
246     * Return list of all known, writable {@link ContactsSource}. Sources
247     * returned may require inflation before they can be used.
248     */
249    public ArrayList<Account> getAccounts(boolean writableOnly) {
250        final AccountManager am = mAccountManager;
251        final Account[] accounts = am.getAccounts();
252        final ArrayList<Account> matching = Lists.newArrayList();
253
254        for (Account account : accounts) {
255            // Ensure we have details loaded for each account
256            final ContactsSource source = getInflatedSource(account.type,
257                    ContactsSource.LEVEL_SUMMARY);
258            final boolean hasContacts = source != null;
259            final boolean matchesWritable = (!writableOnly || (writableOnly && !source.readOnly));
260            if (hasContacts && matchesWritable) {
261                matching.add(account);
262            }
263        }
264        return matching;
265    }
266
267    /**
268     * Find the best {@link DataKind} matching the requested
269     * {@link ContactsSource#accountType} and {@link DataKind#mimeType}. If no
270     * direct match found, we try searching {@link #mFallbackSource}.
271     * When fourceRefresh is set to true, cache is refreshed and inflation of each
272     * EditField will occur.
273     */
274    public DataKind getKindOrFallback(String accountType, String mimeType, Context context,
275            int inflateLevel) {
276        DataKind kind = null;
277
278        // Try finding source and kind matching request
279        final ContactsSource source = mSources.get(accountType);
280        if (source != null) {
281            source.ensureInflated(context, inflateLevel);
282            kind = source.getKindForMimetype(mimeType);
283        }
284
285        if (kind == null) {
286            // Nothing found, so try fallback as last resort
287            mFallbackSource.ensureInflated(context, inflateLevel);
288            kind = mFallbackSource.getKindForMimetype(mimeType);
289        }
290
291        if (kind == null) {
292            Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
293        }
294
295        return kind;
296    }
297
298    /**
299     * Return {@link ContactsSource} for the given account type.
300     */
301    public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
302        // Try finding specific source, otherwise use fallback
303        ContactsSource source = mSources.get(accountType);
304        if (source == null) source = mFallbackSource;
305
306        if (source.isInflated(inflateLevel)) {
307            // Already inflated, so return directly
308            return source;
309        } else {
310            // Not inflated, but requested that we force-inflate
311            source.ensureInflated(mContext, inflateLevel);
312            return source;
313        }
314    }
315}
316