BluetoothMapAccountLoader.java revision eb7b90f5b93db1230a5b64caa3d8d05a642e33a6
1/*
2* Copyright (C) 2014 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15
16package com.android.bluetooth.map;
17
18import java.util.ArrayList;
19import java.util.LinkedHashMap;
20import java.util.List;
21
22import android.content.ContentProviderClient;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.PackageManager;
28import android.content.pm.ProviderInfo;
29import android.content.pm.ResolveInfo;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.RemoteException;
33import android.text.format.DateUtils;
34import android.util.Log;
35
36import com.android.bluetooth.map.BluetoothMapAccountItem;
37import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
38import com.android.bluetooth.mapapi.BluetoothMapContract;
39
40public class BluetoothMapAccountLoader {
41    private static final String TAG = "BluetoothMapAccountLoader";
42    private static final boolean D = BluetoothMapService.DEBUG;
43    private static final boolean V = BluetoothMapService.VERBOSE;
44    private Context mContext = null;
45    private PackageManager mPackageManager = null;
46    private ContentResolver mResolver;
47    private int mAccountsEnabledCount = 0;
48    private ContentProviderClient mProviderClient = null;
49    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
50
51    public BluetoothMapAccountLoader(Context ctx)
52    {
53        mContext = ctx;
54    }
55
56    /**
57     * Method to look through all installed packages system-wide and find those that contain one of
58     * the BT-MAP intents in their manifest file. For each app the list of accounts are fetched
59     * using the method parseAccounts().
60     * @return LinkedHashMap with the packages as keys(BluetoothMapAccountItem) and
61     *          values as ArrayLists of BluetoothMapAccountItems.
62     */
63    public LinkedHashMap<BluetoothMapAccountItem,
64                         ArrayList<BluetoothMapAccountItem>> parsePackages(boolean includeIcon) {
65
66        LinkedHashMap<BluetoothMapAccountItem, ArrayList<BluetoothMapAccountItem>> groups =
67                new LinkedHashMap<BluetoothMapAccountItem,
68                                  ArrayList<BluetoothMapAccountItem>>();
69        Intent[] searchIntents = new Intent[2];
70        //Array <Intent> searchIntents = new Array <Intent>();
71        searchIntents[0] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_EMAIL);
72        searchIntents[1] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_IM);
73        // reset the counter every time this method is called.
74        mAccountsEnabledCount=0;
75        // find all installed packages and filter out those that do not support Bluetooth Map.
76        // this is done by looking for a apps with content providers containing the intent-filter
77        // in the manifest file.
78        mPackageManager = mContext.getPackageManager();
79
80        for (Intent searchIntent : searchIntents) {
81            List<ResolveInfo> resInfos =
82                mPackageManager.queryIntentContentProviders(searchIntent, 0);
83            if (resInfos != null ) {
84                if(D) Log.d(TAG,"Found " + resInfos.size() + " application(s) with intent "
85                        + searchIntent.getAction().toString());
86                BluetoothMapUtils.TYPE msgType = (searchIntent.getAction().toString() ==
87                        BluetoothMapContract.PROVIDER_INTERFACE_EMAIL) ?
88                        BluetoothMapUtils.TYPE.EMAIL : BluetoothMapUtils.TYPE.IM;
89                for (ResolveInfo rInfo : resInfos) {
90                    if(D) Log.d(TAG,"ResolveInfo " + rInfo.toString());
91                    // We cannot rely on apps that have been force-stopped in the
92                    // application settings menu.
93                    if ((rInfo.providerInfo.applicationInfo.flags &
94                            ApplicationInfo.FLAG_STOPPED) == 0) {
95                        BluetoothMapAccountItem app = createAppItem(rInfo, includeIcon, msgType);
96                        if (app != null){
97                            ArrayList<BluetoothMapAccountItem> accounts = parseAccounts(app);
98                            // we do not want to list apps without accounts
99                            if(accounts.size() > 0)
100                            {// we need to make sure that the "select all" checkbox
101                             // is checked if all accounts in the list are checked
102                                app.mIsChecked = true;
103                                for (BluetoothMapAccountItem acc: accounts)
104                                {
105                                    if(!acc.mIsChecked)
106                                    {
107                                        app.mIsChecked = false;
108                                        break;
109                                    }
110                                }
111                                groups.put(app, accounts);
112                            }
113                        }
114                    } else {
115                        if(D)Log.d(TAG,"Ignoring force-stopped authority "
116                                + rInfo.providerInfo.authority +"\n");
117                    }
118                }
119            }
120            else {
121                if(D) Log.d(TAG,"Found no applications");
122            }
123        }
124        return groups;
125    }
126
127    public BluetoothMapAccountItem createAppItem(ResolveInfo rInfo, boolean includeIcon,
128            BluetoothMapUtils.TYPE type) {
129        String provider = rInfo.providerInfo.authority;
130        if(provider != null) {
131            String name = rInfo.loadLabel(mPackageManager).toString();
132            if(D)Log.d(TAG,rInfo.providerInfo.packageName + " - " + name +
133                            " - meta-data(provider = " + provider+")\n");
134            BluetoothMapAccountItem app = BluetoothMapAccountItem.create(
135                    "0",
136                    name,
137                    rInfo.providerInfo.packageName,
138                    provider,
139                    (includeIcon == false)? null : rInfo.loadIcon(mPackageManager),
140                    type);
141            return app;
142        }
143
144        return null;
145    }
146
147    /**
148     * Method for getting the accounts under a given contentprovider from a package.
149     * @param app The parent app object
150     * @return An ArrayList of BluetoothMapAccountItems containing all the accounts from the app
151     */
152    public ArrayList<BluetoothMapAccountItem> parseAccounts(BluetoothMapAccountItem app)  {
153        Cursor c = null;
154        if(D) Log.d(TAG,"Finding accounts for app "+app.getPackageName());
155        ArrayList<BluetoothMapAccountItem> children = new ArrayList<BluetoothMapAccountItem>();
156        // Get the list of accounts from the email apps content resolver (if possible)
157        mResolver = mContext.getContentResolver();
158        try{
159            mProviderClient = mResolver.acquireUnstableContentProviderClient(
160                    Uri.parse(app.mBase_uri_no_account));
161            if (mProviderClient == null) {
162                throw new RemoteException("Failed to acquire provider for " + app.getPackageName());
163            }
164            mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
165
166            Uri uri = Uri.parse(app.mBase_uri_no_account + "/"
167                                + BluetoothMapContract.TABLE_ACCOUNT);
168
169            if(app.getType() == TYPE.IM) {
170                c = mProviderClient.query(uri, BluetoothMapContract.BT_IM_ACCOUNT_PROJECTION,
171                        null, null, BluetoothMapContract.AccountColumns._ID+" DESC");
172            } else {
173                c = mProviderClient.query(uri, BluetoothMapContract.BT_ACCOUNT_PROJECTION,
174                        null, null, BluetoothMapContract.AccountColumns._ID+" DESC");
175            }
176        } catch (RemoteException e){
177            if(D)Log.d(TAG,"Could not establish ContentProviderClient for "+app.getPackageName()+
178                    " - returning empty account list" );
179            return children;
180        } finally {
181            if (mProviderClient != null)
182                mProviderClient.release();
183        }
184
185        if (c != null) {
186            c.moveToPosition(-1);
187            int idIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns._ID);
188            int dispNameIndex = c.getColumnIndex(
189                    BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME);
190            int exposeIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.FLAG_EXPOSE);
191            int uciIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_UCI);
192            int uciPreIndex = c.getColumnIndex(
193                    BluetoothMapContract.AccountColumns.ACCOUNT_UCI_PREFIX);
194            while (c.moveToNext()) {
195                if(D)Log.d(TAG,"Adding account " + c.getString(dispNameIndex) +
196                        " with ID " + String.valueOf(c.getInt(idIndex)));
197                String uci = null;
198                String uciPrefix = null;
199                if(app.getType() == TYPE.IM){
200                    uci = c.getString(uciIndex);
201                    uciPrefix = c.getString(uciPreIndex);
202                    if(D)Log.d(TAG,"   Account UCI " + uci);
203                }
204
205                BluetoothMapAccountItem child = BluetoothMapAccountItem.create(
206                        String.valueOf((c.getInt(idIndex))),
207                        c.getString(dispNameIndex),
208                        app.getPackageName(),
209                        app.getProviderAuthority(),
210                        null,
211                        app.getType(),
212                        uci,
213                        uciPrefix);
214
215                child.mIsChecked = (c.getInt(exposeIndex) != 0);
216                child.mIsChecked = true; // TODO: Revert when this works
217                /* update the account counter
218                 * so we can make sure that not to many accounts are checked. */
219                if(child.mIsChecked)
220                {
221                    mAccountsEnabledCount++;
222                }
223                children.add(child);
224            }
225            c.close();
226        } else {
227            if(D)Log.d(TAG, "query failed");
228        }
229        return children;
230    }
231    /**
232     * Gets the number of enabled accounts in total across all supported apps.
233     * NOTE that this method should not be called before the parsePackages method
234     * has been successfully called.
235     * @return number of enabled accounts
236     */
237    public int getAccountsEnabledCount() {
238        if(D)Log.d(TAG,"Enabled Accounts count:"+ mAccountsEnabledCount);
239        return mAccountsEnabledCount;
240    }
241
242}
243