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