Home | History | Annotate | Download | only in model
      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      */
     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