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