/* * Copyright (C) 2014 Samsung System LSI * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.map; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.text.format.DateUtils; import android.util.Log; import com.android.bluetooth.map.BluetoothMapUtils.TYPE; import com.android.bluetooth.mapapi.BluetoothMapContract; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; public class BluetoothMapAccountLoader { private static final String TAG = "BluetoothMapAccountLoader"; private static final boolean D = BluetoothMapService.DEBUG; private static final boolean V = BluetoothMapService.VERBOSE; private Context mContext = null; private PackageManager mPackageManager = null; private ContentResolver mResolver; private int mAccountsEnabledCount = 0; private ContentProviderClient mProviderClient = null; private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; public BluetoothMapAccountLoader(Context ctx) { mContext = ctx; } /** * Method to look through all installed packages system-wide and find those that contain one of * the BT-MAP intents in their manifest file. For each app the list of accounts are fetched * using the method parseAccounts(). * @return LinkedHashMap with the packages as keys(BluetoothMapAccountItem) and * values as ArrayLists of BluetoothMapAccountItems. */ public LinkedHashMap> parsePackages( boolean includeIcon) { LinkedHashMap> groups = new LinkedHashMap>(); Intent[] searchIntents = new Intent[2]; //Array searchIntents = new Array (); searchIntents[0] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_EMAIL); searchIntents[1] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_IM); // reset the counter every time this method is called. mAccountsEnabledCount = 0; // find all installed packages and filter out those that do not support Bluetooth Map. // this is done by looking for a apps with content providers containing the intent-filter // in the manifest file. mPackageManager = mContext.getPackageManager(); for (Intent searchIntent : searchIntents) { List resInfos = mPackageManager.queryIntentContentProviders(searchIntent, 0); if (resInfos != null) { if (D) { Log.d(TAG, "Found " + resInfos.size() + " application(s) with intent " + searchIntent.getAction()); } BluetoothMapUtils.TYPE msgType = (Objects.equals(searchIntent.getAction(), BluetoothMapContract.PROVIDER_INTERFACE_EMAIL)) ? BluetoothMapUtils.TYPE.EMAIL : BluetoothMapUtils.TYPE.IM; for (ResolveInfo rInfo : resInfos) { if (D) { Log.d(TAG, "ResolveInfo " + rInfo.toString()); } // We cannot rely on apps that have been force-stopped in the // application settings menu. if ((rInfo.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) { BluetoothMapAccountItem app = createAppItem(rInfo, includeIcon, msgType); if (app != null) { ArrayList accounts = parseAccounts(app); // we do not want to list apps without accounts if (accounts.size() > 0) { // we need to make sure that the "select all" checkbox // is checked if all accounts in the list are checked app.mIsChecked = true; for (BluetoothMapAccountItem acc : accounts) { if (!acc.mIsChecked) { app.mIsChecked = false; break; } } groups.put(app, accounts); } } } else { if (D) { Log.d(TAG, "Ignoring force-stopped authority " + rInfo.providerInfo.authority + "\n"); } } } } else { if (D) { Log.d(TAG, "Found no applications"); } } } return groups; } public BluetoothMapAccountItem createAppItem(ResolveInfo rInfo, boolean includeIcon, BluetoothMapUtils.TYPE type) { String provider = rInfo.providerInfo.authority; if (provider != null) { String name = rInfo.loadLabel(mPackageManager).toString(); if (D) { Log.d(TAG, rInfo.providerInfo.packageName + " - " + name + " - meta-data(provider = " + provider + ")\n"); } BluetoothMapAccountItem app = BluetoothMapAccountItem.create("0", name, rInfo.providerInfo.packageName, provider, (!includeIcon) ? null : rInfo.loadIcon(mPackageManager), type); return app; } return null; } /** * Method for getting the accounts under a given contentprovider from a package. * @param app The parent app object * @return An ArrayList of BluetoothMapAccountItems containing all the accounts from the app */ public ArrayList parseAccounts(BluetoothMapAccountItem app) { Cursor c = null; if (D) { Log.d(TAG, "Finding accounts for app " + app.getPackageName()); } ArrayList children = new ArrayList(); // Get the list of accounts from the email apps content resolver (if possible) mResolver = mContext.getContentResolver(); try { mProviderClient = mResolver.acquireUnstableContentProviderClient( Uri.parse(app.mBase_uri_no_account)); if (mProviderClient == null) { throw new RemoteException("Failed to acquire provider for " + app.getPackageName()); } mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); Uri uri = Uri.parse(app.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_ACCOUNT); if (app.getType() == TYPE.IM) { c = mProviderClient.query(uri, BluetoothMapContract.BT_IM_ACCOUNT_PROJECTION, null, null, BluetoothMapContract.AccountColumns._ID + " DESC"); } else { c = mProviderClient.query(uri, BluetoothMapContract.BT_ACCOUNT_PROJECTION, null, null, BluetoothMapContract.AccountColumns._ID + " DESC"); } } catch (RemoteException e) { if (D) { Log.d(TAG, "Could not establish ContentProviderClient for " + app.getPackageName() + " - returning empty account list"); } return children; } finally { if (mProviderClient != null) { mProviderClient.release(); } } if (c != null) { c.moveToPosition(-1); int idIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns._ID); int dispNameIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME); int exposeIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.FLAG_EXPOSE); int uciIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_UCI); int uciPreIndex = c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_UCI_PREFIX); while (c.moveToNext()) { if (D) { Log.d(TAG, "Adding account " + c.getString(dispNameIndex) + " with ID " + String .valueOf(c.getInt(idIndex))); } String uci = null; String uciPrefix = null; if (app.getType() == TYPE.IM) { uci = c.getString(uciIndex); uciPrefix = c.getString(uciPreIndex); if (D) { Log.d(TAG, " Account UCI " + uci); } } BluetoothMapAccountItem child = BluetoothMapAccountItem.create(String.valueOf((c.getInt(idIndex))), c.getString(dispNameIndex), app.getPackageName(), app.getProviderAuthority(), null, app.getType(), uci, uciPrefix); child.mIsChecked = (c.getInt(exposeIndex) != 0); child.mIsChecked = true; // TODO: Revert when this works /* update the account counter * so we can make sure that not to many accounts are checked. */ if (child.mIsChecked) { mAccountsEnabledCount++; } children.add(child); } c.close(); } else { if (D) { Log.d(TAG, "query failed"); } } return children; } /** * Gets the number of enabled accounts in total across all supported apps. * NOTE that this method should not be called before the parsePackages method * has been successfully called. * @return number of enabled accounts */ public int getAccountsEnabledCount() { if (D) { Log.d(TAG, "Enabled Accounts count:" + mAccountsEnabledCount); } return mAccountsEnabledCount; } }