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