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.release(); 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