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
16package com.android.bluetooth.map;
17
18import java.util.ArrayList;
19import java.util.LinkedHashMap;
20import java.util.List;
21
22import com.android.bluetooth.map.BluetoothMapEmailSettingsItem;
23
24
25
26import android.content.ContentProviderClient;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.ApplicationInfo;
31import android.content.pm.PackageInfo;
32import android.content.pm.PackageManager;
33import android.content.pm.PackageManager.NameNotFoundException;
34import android.content.pm.ResolveInfo;
35import android.database.Cursor;
36import android.net.Uri;
37import android.os.RemoteException;
38import com.android.bluetooth.mapapi.BluetoothMapContract;
39import android.text.format.DateUtils;
40import android.util.Log;
41
42
43public class BluetoothMapEmailSettingsLoader {
44    private static final String TAG = "BluetoothMapEmailSettingsLoader";
45    private static final boolean D = BluetoothMapService.DEBUG;
46    private static final boolean V = BluetoothMapService.VERBOSE;
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
54    public BluetoothMapEmailSettingsLoader(Context ctx)
55    {
56        mContext = ctx;
57    }
58
59    /**
60     * Method to look through all installed packages system-wide and find those that contain the
61     * BTMAP meta-tag in their manifest file. For each app the list of accounts are fetched using
62     * the method parseAccounts().
63     * @return LinkedHashMap with the packages as keys(BluetoothMapEmailSettingsItem) and
64     *          values as ArrayLists of BluetoothMapEmailSettingsItems.
65     */
66    public LinkedHashMap<BluetoothMapEmailSettingsItem,
67                         ArrayList<BluetoothMapEmailSettingsItem>> parsePackages(boolean includeIcon) {
68
69        LinkedHashMap<BluetoothMapEmailSettingsItem, ArrayList<BluetoothMapEmailSettingsItem>> groups =
70                new LinkedHashMap<BluetoothMapEmailSettingsItem,ArrayList<BluetoothMapEmailSettingsItem>>();
71        Intent searchIntent = new Intent(BluetoothMapContract.PROVIDER_INTERFACE);
72        // reset the counter every time this method is called.
73        mAccountsEnabledCount=0;
74        // find all installed packages and filter out those that do not support Map Email.
75        // this is done by looking for a apps with content providers containing the intent-filter for
76        // android.content.action.BTMAP_SHARE in the manifest file.
77        mPackageManager = mContext.getPackageManager();
78        List<ResolveInfo> resInfos = mPackageManager
79                .queryIntentContentProviders(searchIntent, 0);
80
81        if (resInfos != null) {
82            if(D) Log.d(TAG,"Found " + resInfos.size() + " applications");
83            for (ResolveInfo rInfo : resInfos) {
84                // We cannot rely on apps that have been force-stopped in the application settings menu.
85                if ((rInfo.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
86                    BluetoothMapEmailSettingsItem app = createAppItem(rInfo, includeIcon);
87                    if (app != null){
88                        ArrayList<BluetoothMapEmailSettingsItem> accounts = parseAccounts(app);
89                        // we do not want to list apps without accounts
90                        if(accounts.size() >0)
91                        {
92                            // we need to make sure that the "select all" checkbox is checked if all accounts in the list are checked
93                            app.mIsChecked = true;
94                            for (BluetoothMapEmailSettingsItem acc: accounts)
95                            {
96                                if(!acc.mIsChecked)
97                                {
98                                    app.mIsChecked = false;
99                                    break;
100                                }
101                            }
102                            groups.put(app, accounts);
103                        }
104                    }
105                } else {
106                    if(D)Log.d(TAG,"Ignoring force-stopped authority "+ rInfo.providerInfo.authority +"\n");
107                }
108            }
109        }
110        else {
111            if(D) Log.d(TAG,"Found no applications");
112        }
113        return groups;
114    }
115
116    public BluetoothMapEmailSettingsItem createAppItem(ResolveInfo rInfo,
117            boolean includeIcon) {
118        String provider = rInfo.providerInfo.authority;
119        if(provider != null) {
120            String name = rInfo.loadLabel(mPackageManager).toString();
121            if(D)Log.d(TAG,rInfo.providerInfo.packageName + " - " + name + " - meta-data(provider = " + provider+")\n");
122            BluetoothMapEmailSettingsItem app = new BluetoothMapEmailSettingsItem(
123                    "0",
124                    name,
125                    rInfo.providerInfo.packageName,
126                    provider,
127                    (includeIcon == false)? null : rInfo.loadIcon(mPackageManager));
128            return app;
129        }
130
131        return null;
132    }
133
134
135    /**
136     * Method for getting the accounts under a given contentprovider from a package.
137     * This
138     * @param app The parent app object
139     * @return An ArrayList of BluetoothMapEmailSettingsItems containing all the accounts from the app
140     */
141    public ArrayList<BluetoothMapEmailSettingsItem> parseAccounts(BluetoothMapEmailSettingsItem app)  {
142        Cursor c = null;
143        if(D) Log.d(TAG,"Adding app "+app.getPackageName());
144        ArrayList<BluetoothMapEmailSettingsItem> children = new ArrayList<BluetoothMapEmailSettingsItem>();
145        // Get the list of accounts from the email apps content resolver (if possible
146        mResolver = mContext.getContentResolver();
147        try{
148            mProviderClient = mResolver.acquireUnstableContentProviderClient(Uri.parse(app.mBase_uri_no_account));
149             if (mProviderClient == null) {
150                 throw new RemoteException("Failed to acquire provider for " + app.getPackageName());
151             }
152             mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
153
154
155             Uri uri = Uri.parse(app.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_ACCOUNT);
156
157             c = mProviderClient.query(uri, BluetoothMapContract.BT_ACCOUNT_PROJECTION, null, null, BluetoothMapContract.AccountColumns._ID+" DESC");
158        } catch (RemoteException e){
159            if(D)Log.d(TAG,"Could not establish ContentProviderClient for "+app.getPackageName()+
160                    " - returning empty account list" );
161            return children;
162        }
163
164        if (c != null) {
165            c.moveToPosition(-1);
166            while (c.moveToNext()) {
167                if(D)Log.d(TAG,"Adding account " +c.getString(c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME))+
168                        " with ID "+String.valueOf((c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns._ID)))));
169
170                BluetoothMapEmailSettingsItem child = new BluetoothMapEmailSettingsItem(
171                        /*id*/           String.valueOf((c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns._ID)))),
172                        /*name*/         c.getString(c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME)) ,
173                        /*package name*/ app.getPackageName(),
174                        /*providerMeta*/ app.getProviderAuthority(),
175                        /*icon*/         null);
176
177                child.mIsChecked = (c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns.FLAG_EXPOSE))!=0);
178                /*update the account counter so we can make sure that not to many accounts are checked. */
179                if(child.mIsChecked)
180                {
181                    mAccountsEnabledCount++;
182                }
183                children.add(child);
184            }
185            c.close();
186        } else {
187            if(D)Log.d(TAG, "query failed");
188        }
189        return children;
190    }
191    /**
192     * Gets the number of enabled accounts in total across all supported apps.
193     * NOTE that this method should not be called before the parsePackages method
194     * has been successfully called.
195     * @return number of enabled accounts
196     */
197    public int getAccountsEnabledCount() {
198        if(D)Log.d(TAG,"Enabled Accounts count:"+ mAccountsEnabledCount);
199        return mAccountsEnabledCount;
200    }
201
202}
203