1/*
2 * Copyright (C) 2015 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.settingslib.accounts;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.accounts.AuthenticatorDescription;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.SyncAdapterType;
28import android.content.pm.PackageManager;
29import android.content.res.Resources;
30import android.graphics.drawable.Drawable;
31import android.os.AsyncTask;
32import android.os.UserHandle;
33import android.util.Log;
34
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.Map;
38
39/**
40 * Helper class for monitoring accounts on the device for a given user.
41 *
42 * Classes using this helper should implement {@link OnAccountsUpdateListener}.
43 * {@link OnAccountsUpdateListener#onAccountsUpdate(UserHandle)} will then be
44 * called once accounts get updated. For setting up listening for account
45 * updates, {@link #listenToAccountUpdates()} and
46 * {@link #stopListeningToAccountUpdates()} should be used.
47 */
48final public class AuthenticatorHelper extends BroadcastReceiver {
49    private static final String TAG = "AuthenticatorHelper";
50
51    private final Map<String, AuthenticatorDescription> mTypeToAuthDescription = new HashMap<>();
52    private final ArrayList<String> mEnabledAccountTypes = new ArrayList<>();
53    private final Map<String, Drawable> mAccTypeIconCache = new HashMap<>();
54    private final HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = new HashMap<>();
55
56    private final UserHandle mUserHandle;
57    private final Context mContext;
58    private final OnAccountsUpdateListener mListener;
59    private boolean mListeningToAccountUpdates;
60
61    public interface OnAccountsUpdateListener {
62        void onAccountsUpdate(UserHandle userHandle);
63    }
64
65    public AuthenticatorHelper(Context context, UserHandle userHandle,
66            OnAccountsUpdateListener listener) {
67        mContext = context;
68        mUserHandle = userHandle;
69        mListener = listener;
70        // This guarantees that the helper is ready to use once constructed: the account types and
71        // authorities are initialized
72        onAccountsUpdated(null);
73    }
74
75    public String[] getEnabledAccountTypes() {
76        return mEnabledAccountTypes.toArray(new String[mEnabledAccountTypes.size()]);
77    }
78
79    public void preloadDrawableForType(final Context context, final String accountType) {
80        new AsyncTask<Void, Void, Void>() {
81            @Override
82            protected Void doInBackground(Void... params) {
83                getDrawableForType(context, accountType);
84                return null;
85            }
86        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
87    }
88
89    /**
90     * Gets an icon associated with a particular account type. If none found, return null.
91     * @param accountType the type of account
92     * @return a drawable for the icon or a default icon returned by
93     * {@link PackageManager#getDefaultActivityIcon} if one cannot be found.
94     */
95    public Drawable getDrawableForType(Context context, final String accountType) {
96        Drawable icon = null;
97        synchronized (mAccTypeIconCache) {
98            if (mAccTypeIconCache.containsKey(accountType)) {
99                return mAccTypeIconCache.get(accountType);
100            }
101        }
102        if (mTypeToAuthDescription.containsKey(accountType)) {
103            try {
104                AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
105                Context authContext = context.createPackageContextAsUser(desc.packageName, 0,
106                        mUserHandle);
107                icon = mContext.getPackageManager().getUserBadgedIcon(
108                        authContext.getDrawable(desc.iconId), mUserHandle);
109                synchronized (mAccTypeIconCache) {
110                    mAccTypeIconCache.put(accountType, icon);
111                }
112            } catch (PackageManager.NameNotFoundException|Resources.NotFoundException e) {
113                // Ignore
114            }
115        }
116        if (icon == null) {
117            icon = context.getPackageManager().getDefaultActivityIcon();
118        }
119        return icon;
120    }
121
122    /**
123     * Gets the label associated with a particular account type. If none found, return null.
124     * @param accountType the type of account
125     * @return a CharSequence for the label or null if one cannot be found.
126     */
127    public CharSequence getLabelForType(Context context, final String accountType) {
128        CharSequence label = null;
129        if (mTypeToAuthDescription.containsKey(accountType)) {
130            try {
131                AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
132                Context authContext = context.createPackageContextAsUser(desc.packageName, 0,
133                        mUserHandle);
134                label = authContext.getResources().getText(desc.labelId);
135            } catch (PackageManager.NameNotFoundException e) {
136                Log.w(TAG, "No label name for account type " + accountType);
137            } catch (Resources.NotFoundException e) {
138                Log.w(TAG, "No label icon for account type " + accountType);
139            }
140        }
141        return label;
142    }
143
144    /**
145     * Gets the package associated with a particular account type. If none found, return null.
146     * @param accountType the type of account
147     * @return the package name or null if one cannot be found.
148     */
149    public String getPackageForType(final String accountType) {
150        if (mTypeToAuthDescription.containsKey(accountType)) {
151            AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
152            return desc.packageName;
153        }
154        return null;
155    }
156
157    /**
158     * Gets the resource id of the label associated with a particular account type. If none found,
159     * return -1.
160     * @param accountType the type of account
161     * @return a resource id for the label or -1 if none found;
162     */
163    public int getLabelIdForType(final String accountType) {
164        if (mTypeToAuthDescription.containsKey(accountType)) {
165            AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
166            return desc.labelId;
167        }
168        return -1;
169    }
170
171    /**
172     * Updates provider icons. Subclasses should call this in onCreate()
173     * and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated().
174     */
175    public void updateAuthDescriptions(Context context) {
176        AuthenticatorDescription[] authDescs = AccountManager.get(context)
177                .getAuthenticatorTypesAsUser(mUserHandle.getIdentifier());
178        for (int i = 0; i < authDescs.length; i++) {
179            mTypeToAuthDescription.put(authDescs[i].type, authDescs[i]);
180        }
181    }
182
183    public boolean containsAccountType(String accountType) {
184        return mTypeToAuthDescription.containsKey(accountType);
185    }
186
187    public AuthenticatorDescription getAccountTypeDescription(String accountType) {
188        return mTypeToAuthDescription.get(accountType);
189    }
190
191    public boolean hasAccountPreferences(final String accountType) {
192        if (containsAccountType(accountType)) {
193            AuthenticatorDescription desc = getAccountTypeDescription(accountType);
194            if (desc != null && desc.accountPreferencesId != 0) {
195                return true;
196            }
197        }
198        return false;
199    }
200
201    void onAccountsUpdated(Account[] accounts) {
202        updateAuthDescriptions(mContext);
203        if (accounts == null) {
204            accounts = AccountManager.get(mContext).getAccountsAsUser(mUserHandle.getIdentifier());
205        }
206        mEnabledAccountTypes.clear();
207        mAccTypeIconCache.clear();
208        for (int i = 0; i < accounts.length; i++) {
209            final Account account = accounts[i];
210            if (!mEnabledAccountTypes.contains(account.type)) {
211                mEnabledAccountTypes.add(account.type);
212            }
213        }
214        buildAccountTypeToAuthoritiesMap();
215        if (mListeningToAccountUpdates) {
216            mListener.onAccountsUpdate(mUserHandle);
217        }
218    }
219
220    @Override
221    public void onReceive(final Context context, final Intent intent) {
222        // TODO: watch for package upgrades to invalidate cache; see http://b/7206643
223        final Account[] accounts = AccountManager.get(mContext)
224                .getAccountsAsUser(mUserHandle.getIdentifier());
225        onAccountsUpdated(accounts);
226    }
227
228    public void listenToAccountUpdates() {
229        if (!mListeningToAccountUpdates) {
230            IntentFilter intentFilter = new IntentFilter();
231            intentFilter.addAction(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
232            // At disk full, certain actions are blocked (such as writing the accounts to storage).
233            // It is useful to also listen for recovery from disk full to avoid bugs.
234            intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
235            mContext.registerReceiverAsUser(this, mUserHandle, intentFilter, null, null);
236            mListeningToAccountUpdates = true;
237        }
238    }
239
240    public void stopListeningToAccountUpdates() {
241        if (mListeningToAccountUpdates) {
242            mContext.unregisterReceiver(this);
243            mListeningToAccountUpdates = false;
244        }
245    }
246
247    public ArrayList<String> getAuthoritiesForAccountType(String type) {
248        return mAccountTypeToAuthorities.get(type);
249    }
250
251    private void buildAccountTypeToAuthoritiesMap() {
252        mAccountTypeToAuthorities.clear();
253        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
254                mUserHandle.getIdentifier());
255        for (int i = 0, n = syncAdapters.length; i < n; i++) {
256            final SyncAdapterType sa = syncAdapters[i];
257            ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType);
258            if (authorities == null) {
259                authorities = new ArrayList<String>();
260                mAccountTypeToAuthorities.put(sa.accountType, authorities);
261            }
262            if (Log.isLoggable(TAG, Log.VERBOSE)) {
263                Log.v(TAG, "Added authority " + sa.authority + " to accountType "
264                        + sa.accountType);
265            }
266            authorities.add(sa.authority);
267        }
268    }
269}
270