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