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