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