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.loaderapp.model;
18 
19 import com.android.loaderapp.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 import 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  */
51 public 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      */
getInstance(Context context)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      */
Sources(Context context)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 */
Sources(ContactsSource... sources)112     public Sources(ContactsSource... sources) {
113         for (ContactsSource source : sources) {
114             addSource(source);
115         }
116     }
117 
addSource(ContactsSource source)118     protected void addSource(ContactsSource source) {
119         mSources.put(source.accountType, source);
120         mKnownPackages.add(source.resPackageName);
121     }
122 
123     /** {@inheritDoc} */
124     @Override
onReceive(Context context, Intent intent)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 
invalidateCache(String packageName)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 
invalidateAllCache()168     protected void invalidateAllCache() {
169         mFallbackSource.invalidateCache();
170         for (ContactsSource source : mSources.values()) {
171             source.invalidateCache();
172         }
173     }
174 
175     /** {@inheritDoc} */
onAccountsUpdated(Account[] accounts)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      */
queryAccounts()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      */
findAuthenticator(AuthenticatorDescription[] auths, String accountType)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      */
getAccounts(boolean writableOnly)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      */
getKindOrFallback(String accountType, String mimeType, Context context, int inflateLevel)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      */
getInflatedSource(String accountType, int inflateLevel)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