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*/
15package com.android.bluetooth.map;
16
17import java.util.ArrayList;
18import java.util.LinkedHashMap;
19import java.util.List;
20
21import android.content.BroadcastReceiver;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.database.ContentObserver;
29import android.net.Uri;
30import android.util.Log;
31
32import com.android.bluetooth.mapapi.BluetoothMapContract;
33
34/**
35 * Class to construct content observers for for email applications on the system.
36 *
37 *
38 */
39
40public class BluetoothMapAppObserver{
41
42    private static final String TAG = "BluetoothMapAppObserver";
43
44    private static final boolean D = BluetoothMapService.DEBUG;
45    private static final boolean V = BluetoothMapService.VERBOSE;
46    /*  */
47    private LinkedHashMap<BluetoothMapAccountItem, ArrayList<BluetoothMapAccountItem>> mFullList;
48    private LinkedHashMap<String,ContentObserver> mObserverMap =
49            new LinkedHashMap<String,ContentObserver>();
50    private ContentResolver mResolver;
51    private Context mContext;
52    private BroadcastReceiver mReceiver;
53    private PackageManager mPackageManager = null;
54    BluetoothMapAccountLoader mLoader;
55    BluetoothMapService mMapService = null;
56    private boolean mRegisteredReceiver = false;
57
58    public BluetoothMapAppObserver(final Context context, BluetoothMapService mapService) {
59        mContext    = context;
60        mMapService = mapService;
61        mResolver   = context.getContentResolver();
62        mLoader     = new BluetoothMapAccountLoader(mContext);
63        mFullList   = mLoader.parsePackages(false); /* Get the current list of apps */
64        createReceiver();
65        initObservers();
66    }
67
68
69    private BluetoothMapAccountItem getApp(String authoritiesName) {
70        if(V) Log.d(TAG, "getApp(): Looking for " + authoritiesName);
71        for(BluetoothMapAccountItem app:mFullList.keySet()){
72            if(V) Log.d(TAG, "  Comparing: " + app.getProviderAuthority());
73            if(app.getProviderAuthority().equals(authoritiesName)) {
74                if(V) Log.d(TAG, "  found " + app.mBase_uri_no_account);
75                return app;
76            }
77        }
78        if(V) Log.d(TAG, "  NOT FOUND!");
79        return null;
80    }
81
82    private void handleAccountChanges(String packageNameWithProvider) {
83
84        if(D)Log.d(TAG,"handleAccountChanges (packageNameWithProvider: "
85                        +packageNameWithProvider+"\n");
86        //String packageName = packageNameWithProvider.replaceFirst("\\.[^\\.]+$", "");
87        BluetoothMapAccountItem app = getApp(packageNameWithProvider);
88        if(app != null) {
89            ArrayList<BluetoothMapAccountItem> newAccountList = mLoader.parseAccounts(app);
90            ArrayList<BluetoothMapAccountItem> oldAccountList = mFullList.get(app);
91            ArrayList<BluetoothMapAccountItem> addedAccountList =
92                    (ArrayList<BluetoothMapAccountItem>)newAccountList.clone();
93            // Same as oldAccountList.clone
94            ArrayList<BluetoothMapAccountItem> removedAccountList = mFullList.get(app);
95            if (oldAccountList == null)
96                oldAccountList = new ArrayList <BluetoothMapAccountItem>();
97            if (removedAccountList == null)
98                removedAccountList = new ArrayList <BluetoothMapAccountItem>();
99
100            mFullList.put(app, newAccountList);
101            for(BluetoothMapAccountItem newAcc: newAccountList){
102                for(BluetoothMapAccountItem oldAcc: oldAccountList){
103                    if(newAcc.getId() == oldAcc.getId()){
104                        // For each match remove from both removed and added lists
105                        removedAccountList.remove(oldAcc);
106                        addedAccountList.remove(newAcc);
107                        if(!newAcc.getName().equals(oldAcc.getName()) && newAcc.mIsChecked){
108                            // Name Changed and the acc is visible - Change Name in SDP record
109                            mMapService.updateMasInstances(
110                                    BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED);
111                            if(V)Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED");
112                        }
113                        if(newAcc.mIsChecked != oldAcc.mIsChecked) {
114                            // Visibility changed
115                            if(newAcc.mIsChecked){
116                                // account added - create SDP record
117                                mMapService.updateMasInstances(
118                                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
119                                if(V)Log.d(TAG, "UPDATE_MAS_INSTANCES_ACCOUNT_ADDED " +
120                                        "isChecked changed");
121                            } else {
122                                // account removed - remove SDP record
123                                mMapService.updateMasInstances(
124                                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
125                                if(V)Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED " +
126                                        "isChecked changed");
127                            }
128                        }
129                        break;
130                    }
131                }
132            }
133            // Notify on any removed accounts
134            for(BluetoothMapAccountItem removedAcc: removedAccountList){
135                mMapService.updateMasInstances(
136                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
137                if(V)Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED " + removedAcc);
138            }
139            // Notify on any new accounts
140            for(BluetoothMapAccountItem addedAcc: addedAccountList){
141                mMapService.updateMasInstances(
142                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
143                if(V)Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_ADDED " + addedAcc);
144            }
145
146        } else {
147            Log.e(TAG, "Received change notification on package not registered for notifications!");
148
149        }
150    }
151
152    /**
153     * Adds a new content observer to the list of content observers.
154     * The key for the observer is the uri as string
155     * @param uri uri for the package that supports MAP email
156     */
157
158    public void registerObserver(BluetoothMapAccountItem app) {
159        Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
160        if (V) Log.d(TAG, "registerObserver for URI "+uri.toString()+"\n");
161        ContentObserver observer = new ContentObserver(null) {
162            @Override
163            public void onChange(boolean selfChange) {
164                onChange(selfChange, null);
165            }
166
167            @Override
168            public void onChange(boolean selfChange, Uri uri) {
169                if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
170                        + " Uri: " + uri + " selfchange: " + selfChange);
171                if(uri != null) {
172                    handleAccountChanges(uri.getHost());
173                } else {
174                    Log.e(TAG, "Unable to handle change as the URI is NULL!");
175                }
176
177            }
178        };
179        mObserverMap.put(uri.toString(), observer);
180        //False "notifyForDescendents" : Get notified whenever a change occurs to the exact URI.
181        mResolver.registerContentObserver(uri, false, observer);
182    }
183
184    public void unregisterObserver(BluetoothMapAccountItem app) {
185        Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
186        if (V) Log.d(TAG, "unregisterObserver("+uri.toString()+")\n");
187        mResolver.unregisterContentObserver(mObserverMap.get(uri.toString()));
188        mObserverMap.remove(uri.toString());
189    }
190
191    private void initObservers(){
192        if(D)Log.d(TAG,"initObservers()");
193        for(BluetoothMapAccountItem app: mFullList.keySet()){
194            registerObserver(app);
195        }
196    }
197
198    private void deinitObservers(){
199        if(D)Log.d(TAG,"deinitObservers()");
200        for(BluetoothMapAccountItem app: mFullList.keySet()){
201            unregisterObserver(app);
202        }
203    }
204
205    private void createReceiver(){
206        if(D)Log.d(TAG,"createReceiver()\n");
207        IntentFilter intentFilter = new IntentFilter();
208        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
209        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
210        intentFilter.addDataScheme("package");
211        mReceiver = new BroadcastReceiver() {
212            @Override
213            public void onReceive(Context context, Intent intent) {
214                if(D)Log.d(TAG,"onReceive\n");
215                String action = intent.getAction();
216
217                if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
218                    Uri data = intent.getData();
219                    String packageName = data.getEncodedSchemeSpecificPart();
220                    if(D)Log.d(TAG,"The installed package is: "+ packageName);
221
222                    BluetoothMapUtils.TYPE msgType = BluetoothMapUtils.TYPE.NONE;
223                    ResolveInfo resolveInfo = null;
224                    Intent[] searchIntents = new Intent[2];
225                    //Array <Intent> searchIntents = new Array <Intent>();
226                    searchIntents[0] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_EMAIL);
227                    searchIntents[1] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_IM);
228                    // Find all installed packages and filter out those that support Bluetooth Map.
229
230                    mPackageManager = mContext.getPackageManager();
231
232                    for (Intent searchIntent : searchIntents) {
233                        List<ResolveInfo> resInfos =
234                                mPackageManager.queryIntentContentProviders(searchIntent, 0);
235                        if (resInfos != null ) {
236                            if(D) Log.d(TAG,"Found " + resInfos.size()
237                                    + " application(s) with intent "
238                                    + searchIntent.getAction().toString());
239                            for (ResolveInfo rInfo : resInfos) {
240                                if(rInfo != null) {
241                                    // Find out if package contain Bluetooth MAP support
242                                    if (packageName.equals(rInfo.providerInfo.packageName)) {
243                                        resolveInfo = rInfo;
244                                        if(searchIntent.getAction() ==
245                                                BluetoothMapContract.PROVIDER_INTERFACE_EMAIL){
246                                            msgType = BluetoothMapUtils.TYPE.EMAIL;
247                                        } else if (searchIntent.getAction() ==
248                                                BluetoothMapContract.PROVIDER_INTERFACE_IM){
249                                            msgType = BluetoothMapUtils.TYPE.IM;
250                                        }
251                                        break;
252                                    }
253                                }
254                            }
255                        }
256                    }
257                    // if application found with Bluetooth MAP support add to list
258                    if(resolveInfo != null) {
259                        if(D) Log.d(TAG,"Found " + resolveInfo.providerInfo.packageName
260                                + " application of type " + msgType);
261                        BluetoothMapAccountItem app = mLoader.createAppItem(resolveInfo,
262                                false, msgType);
263                        if(app != null) {
264                            registerObserver(app);
265                            // Add all accounts to mFullList
266                            ArrayList<BluetoothMapAccountItem> newAccountList =
267                                    mLoader.parseAccounts(app);
268                            mFullList.put(app, newAccountList);
269                        }
270                    }
271
272                }
273                else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
274                    Uri data = intent.getData();
275                    String packageName = data.getEncodedSchemeSpecificPart();
276                    if(D)Log.d(TAG,"The removed package is: "+ packageName);
277                    BluetoothMapAccountItem app = getApp(packageName);
278                    /* Find the object and remove from fullList */
279                    if(app != null) {
280                        unregisterObserver(app);
281                        mFullList.remove(app);
282                    }
283                }
284            }
285        };
286        if (!mRegisteredReceiver) {
287            try {
288                mContext.registerReceiver(mReceiver,intentFilter);
289                mRegisteredReceiver = true;
290            } catch (Exception e) {
291                Log.e(TAG,"Unable to register MapAppObserver receiver", e);
292            }
293        }
294    }
295
296    private void removeReceiver(){
297        if(D)Log.d(TAG,"removeReceiver()\n");
298        if (mRegisteredReceiver) {
299            try {
300                mRegisteredReceiver = false;
301                mContext.unregisterReceiver(mReceiver);
302            } catch (Exception e) {
303                Log.e(TAG,"Unable to unregister mapAppObserver receiver", e);
304            }
305        }
306    }
307
308    /**
309     * Method to get a list of the accounts (across all apps) that are set to be shared
310     * through MAP.
311     * @return Arraylist<BluetoothMapAccountItem> containing all enabled accounts
312     */
313    public ArrayList<BluetoothMapAccountItem> getEnabledAccountItems(){
314        if(D)Log.d(TAG,"getEnabledAccountItems()\n");
315        ArrayList<BluetoothMapAccountItem> list = new ArrayList<BluetoothMapAccountItem>();
316        for (BluetoothMapAccountItem app:mFullList.keySet()){
317            if (app != null) {
318                ArrayList<BluetoothMapAccountItem> accountList = mFullList.get(app);
319                if (accountList != null) {
320                    for (BluetoothMapAccountItem acc: accountList) {
321                        if (acc.mIsChecked) {
322                            list.add(acc);
323                        }
324                    }
325                } else {
326                    Log.w(TAG,"getEnabledAccountItems() - No AccountList enabled\n");
327                }
328            } else {
329                Log.w(TAG,"getEnabledAccountItems() - No Account in App enabled\n");
330            }
331        }
332        return list;
333    }
334
335    /**
336     * Method to get a list of the accounts (across all apps).
337     * @return Arraylist<BluetoothMapAccountItem> containing all accounts
338     */
339    public ArrayList<BluetoothMapAccountItem> getAllAccountItems(){
340        if(D)Log.d(TAG,"getAllAccountItems()\n");
341        ArrayList<BluetoothMapAccountItem> list = new ArrayList<BluetoothMapAccountItem>();
342        for(BluetoothMapAccountItem app:mFullList.keySet()){
343            ArrayList<BluetoothMapAccountItem> accountList = mFullList.get(app);
344            list.addAll(accountList);
345        }
346        return list;
347    }
348
349
350    /**
351     * Cleanup all resources - must be called to avoid leaks.
352     */
353    public void shutdown() {
354        deinitObservers();
355        removeReceiver();
356    }
357}
358