• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.mapclient;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothMapClient;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.provider.Telephony;
28 import android.provider.Telephony.Mms;
29 import android.provider.Telephony.MmsSms;
30 import android.provider.Telephony.Sms;
31 import android.provider.Telephony.Threads;
32 import android.telephony.PhoneNumberUtils;
33 import android.telephony.SubscriptionInfo;
34 import android.telephony.SubscriptionManager;
35 import android.telephony.TelephonyManager;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import com.android.bluetooth.Utils;
40 import com.android.bluetooth.map.BluetoothMapbMessageMime;
41 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
42 import com.android.vcard.VCardConstants;
43 import com.android.vcard.VCardEntry;
44 import com.android.vcard.VCardProperty;
45 
46 import com.google.android.mms.pdu.PduHeaders;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Set;
53 
54 class MapClientContent {
55 
56     private static final String INBOX_PATH = "telecom/msg/inbox";
57     private static final String TAG = "MapClientContent";
58     private static final int DEFAULT_CHARSET = 106;
59     private static final int ORIGINATOR_ADDRESS_TYPE = 137;
60     private static final int RECIPIENT_ADDRESS_TYPE = 151;
61 
62     final BluetoothDevice mDevice;
63     private final Context mContext;
64     private final Callbacks mCallbacks;
65     private final ContentResolver mResolver;
66     ContentObserver mContentObserver;
67     String mPhoneNumber = null;
68     private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
69     private SubscriptionManager mSubscriptionManager;
70     private TelephonyManager mTelephonyManager;
71     private HashMap<String, Uri> mHandleToUriMap = new HashMap<>();
72     private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>();
73 
74     /**
75      * Callbacks
76      * API to notify about statusChanges as observed from the content provider
77      */
78     interface Callbacks {
onMessageStatusChanged(String handle, int status)79         void onMessageStatusChanged(String handle, int status);
80     }
81 
82     /**
83      * MapClientContent manages all interactions between Bluetooth and the messaging provider.
84      *
85      * Changes to the database are mirrored between the remote and local providers, specifically new
86      * messages, changes to read status, and removal of messages.
87      *
88      * context: the context that all content provider interactions are conducted
89      * MceStateMachine:  the interface to send outbound updates such as when a message is read
90      * locally
91      * device: the associated Bluetooth device used for associating messages with a subscription
92      */
MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)93     MapClientContent(Context context, Callbacks callbacks,
94             BluetoothDevice device) {
95         mContext = context;
96         mDevice = device;
97         mCallbacks = callbacks;
98         mResolver = mContext.getContentResolver();
99 
100         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
101         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
102         mSubscriptionManager
103                 .addSubscriptionInfoRecord(mDevice.getAddress(), Utils.getName(mDevice), 0,
104                         SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
105         SubscriptionInfo info = mSubscriptionManager
106                 .getActiveSubscriptionInfoForIcc(mDevice.getAddress());
107         if (info != null) {
108             mSubscriptionId = info.getSubscriptionId();
109         }
110 
111         mContentObserver = new ContentObserver(null) {
112             @Override
113             public boolean deliverSelfNotifications() {
114                 return false;
115             }
116 
117             @Override
118             public void onChange(boolean selfChange) {
119                 logV("onChange");
120                 findChangeInDatabase();
121             }
122 
123             @Override
124             public void onChange(boolean selfChange, Uri uri) {
125                 logV("onChange" + uri.toString());
126                 findChangeInDatabase();
127             }
128         };
129 
130         clearMessages(mContext, mSubscriptionId);
131         mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver);
132         mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver);
133         mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver);
134     }
135 
clearAllContent(Context context)136     static void clearAllContent(Context context) {
137         SubscriptionManager subscriptionManager =
138                 context.getSystemService(SubscriptionManager.class);
139         List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
140         if (subscriptions == null) {
141             Log.w(TAG, "Active subscription list is missing");
142             return;
143         }
144         for (SubscriptionInfo info : subscriptions) {
145             if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) {
146                 clearMessages(context, info.getSubscriptionId());
147                 try {
148                     subscriptionManager.removeSubscriptionInfoRecord(info.getIccId(),
149                             SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
150                 } catch (Exception e) {
151                     Log.w(TAG, "cleanUp failed: " + e.toString());
152                 }
153             }
154         }
155     }
156 
logD(String message)157     private static void logD(String message) {
158         if (MapClientService.DBG) {
159             Log.d(TAG, message);
160         }
161     }
162 
logV(String message)163     private static void logV(String message) {
164         if (MapClientService.VDBG) {
165             Log.v(TAG, message);
166         }
167     }
168 
169     /**
170      * parseLocalNumber
171      *
172      * Determine the connected phone's number by extracting it from an inbound or outbound mms
173      * message.  This number is necessary such that group messages can be displayed correctly.
174      */
parseLocalNumber(Bmessage message)175     void parseLocalNumber(Bmessage message) {
176         if (mPhoneNumber != null) {
177             return;
178         }
179         if (INBOX_PATH.equals(message.getFolder())) {
180             ArrayList<VCardEntry> recipients = message.getRecipients();
181             if (recipients != null && !recipients.isEmpty()) {
182                 mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(
183                         getFirstRecipientNumber(message));
184             }
185         } else {
186             mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
187         }
188 
189         logV("Found phone number: " + mPhoneNumber);
190     }
191 
192     /**
193      * storeMessage
194      *
195      * Store a message in database with the associated handle and timestamp.
196      * The handle is used to associate the local message with the remote message.
197      */
storeMessage(Bmessage message, String handle, Long timestamp)198     void storeMessage(Bmessage message, String handle, Long timestamp) {
199         switch (message.getType()) {
200             case MMS:
201                 storeMms(message, handle, timestamp);
202                 return;
203             case SMS_CDMA:
204             case SMS_GSM:
205                 storeSms(message, handle, timestamp);
206                 return;
207             default:
208                 logD("Request to store unsupported message type: " + message.getType());
209         }
210     }
211 
storeSms(Bmessage message, String handle, Long timestamp)212     private void storeSms(Bmessage message, String handle, Long timestamp) {
213         logD("storeSms");
214         logV(message.toString());
215         VCardEntry originator = message.getOriginator();
216         String recipients;
217         if (INBOX_PATH.equals(message.getFolder())) {
218             recipients = getOriginatorNumber(message);
219         } else {
220             recipients = getFirstRecipientNumber(message);
221             if (recipients == null) {
222                 logD("invalid recipients");
223                 return;
224             }
225         }
226         logV("Received SMS from Number " + recipients);
227         String messageContent;
228 
229         Uri contentUri = INBOX_PATH.equalsIgnoreCase(message.getFolder()) ? Sms.Inbox.CONTENT_URI
230                 : Sms.Sent.CONTENT_URI;
231         ContentValues values = new ContentValues();
232         long threadId = getThreadId(message);
233         int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
234 
235         values.put(Sms.THREAD_ID, threadId);
236         values.put(Sms.ADDRESS, recipients);
237         values.put(Sms.BODY, message.getBodyContent());
238         values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId);
239         values.put(Sms.DATE, timestamp);
240         values.put(Sms.READ, readStatus);
241 
242         Uri results = mResolver.insert(contentUri, values);
243         mHandleToUriMap.put(handle, results);
244         mUriToHandleMap.put(results, new MessageStatus(handle, readStatus));
245         logD("Map InsertedThread" + results);
246     }
247 
248     /**
249      * deleteMessage
250      * remove a message from the local provider based on a remote change
251      */
deleteMessage(String handle)252     void deleteMessage(String handle) {
253         logD("deleting handle" + handle);
254         Uri messageToChange = mHandleToUriMap.get(handle);
255         if (messageToChange != null) {
256             mResolver.delete(messageToChange, null);
257         }
258     }
259 
260 
261     /**
262      * markRead
263      * mark a message read in the local provider based on a remote change
264      */
markRead(String handle)265     void markRead(String handle) {
266         logD("marking read " + handle);
267         Uri messageToChange = mHandleToUriMap.get(handle);
268         if (messageToChange != null) {
269             ContentValues values = new ContentValues();
270             values.put(Sms.READ, 1);
271             mResolver.update(messageToChange, values, null);
272         }
273     }
274 
275     /**
276      * findChangeInDatabase
277      * compare the current state of the local content provider to the expected state and propagate
278      * changes to the remote.
279      */
findChangeInDatabase()280     private void findChangeInDatabase() {
281         HashMap<Uri, MessageStatus> originalUriToHandleMap;
282         HashMap<Uri, MessageStatus> duplicateUriToHandleMap;
283 
284         originalUriToHandleMap = mUriToHandleMap;
285         duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap);
286         for (Uri uri : new Uri[]{Mms.CONTENT_URI, Sms.CONTENT_URI}) {
287             Cursor cursor = mResolver.query(uri, null, null, null, null);
288             while (cursor.moveToNext()) {
289                 Uri index = Uri
290                         .withAppendedPath(uri, cursor.getString(cursor.getColumnIndex("_id")));
291                 int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ));
292                 MessageStatus currentMessage = duplicateUriToHandleMap.remove(index);
293                 if (currentMessage != null && currentMessage.mRead != readStatus) {
294                     logV(currentMessage.mHandle);
295                     currentMessage.mRead = readStatus;
296                     mCallbacks.onMessageStatusChanged(currentMessage.mHandle,
297                             BluetoothMapClient.READ);
298                 }
299             }
300         }
301         for (HashMap.Entry record : duplicateUriToHandleMap.entrySet()) {
302             logV("Deleted " + ((MessageStatus) record.getValue()).mHandle);
303             originalUriToHandleMap.remove(record.getKey());
304             mCallbacks.onMessageStatusChanged(((MessageStatus) record.getValue()).mHandle,
305                     BluetoothMapClient.DELETED);
306         }
307     }
308 
storeMms(Bmessage message, String handle, Long timestamp)309     private void storeMms(Bmessage message, String handle, Long timestamp) {
310         logD("storeMms");
311         logV(message.toString());
312         try {
313             parseLocalNumber(message);
314             ContentValues values = new ContentValues();
315             long threadId = getThreadId(message);
316             BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
317             mmsBmessage.parseMsgPart(message.getBodyContent());
318             int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
319             Uri contentUri;
320             int messageBox;
321             if (INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
322                 contentUri = Mms.Inbox.CONTENT_URI;
323                 messageBox = Mms.MESSAGE_BOX_INBOX;
324             } else {
325                 contentUri = Mms.Sent.CONTENT_URI;
326                 messageBox = Mms.MESSAGE_BOX_SENT;
327             }
328             logD("Parsed");
329             values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
330             values.put(Mms.THREAD_ID, threadId);
331             values.put(Mms.DATE, timestamp / 1000L);
332             values.put(Mms.TEXT_ONLY, true);
333             values.put(Mms.MESSAGE_BOX, messageBox);
334             values.put(Mms.READ, read);
335             values.put(Mms.SEEN, 0);
336             values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
337             values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
338             values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
339             values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
340             values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
341             values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
342             values.put(Mms.LOCKED, 0);
343             values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
344             values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
345             values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize());
346 
347             Uri results = mResolver.insert(contentUri, values);
348             mHandleToUriMap.put(handle, results);
349             mUriToHandleMap.put(results, new MessageStatus(handle, read));
350 
351             logD("Map InsertedThread" + results);
352 
353             for (MimePart part : mmsBmessage.getMimeParts()) {
354                 storeMmsPart(part, results);
355             }
356 
357             storeAddressPart(message, results);
358 
359             String messageContent = mmsBmessage.getMessageAsText();
360 
361             values.put(Mms.Part.CONTENT_TYPE, "plain/text");
362             values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
363         } catch (Exception e) {
364             Log.e(TAG, e.toString());
365             throw e;
366         }
367     }
368 
storeMmsPart(MimePart messagePart, Uri messageUri)369     private Uri storeMmsPart(MimePart messagePart, Uri messageUri) {
370         ContentValues values = new ContentValues();
371         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
372         values.put(Mms.Part.CHARSET, DEFAULT_CHARSET);
373         values.put(Mms.Part.FILENAME, "text_1.txt");
374         values.put(Mms.Part.NAME, "text_1.txt");
375         values.put(Mms.Part.CONTENT_ID, messagePart.mContentId);
376         values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation);
377         values.put(Mms.Part.TEXT, messagePart.getDataAsString());
378 
379         Uri contentUri = Uri.parse(messageUri.toString() + "/part");
380         Uri results = mResolver.insert(contentUri, values);
381         logD("Inserted" + results);
382         return results;
383     }
384 
storeAddressPart(Bmessage message, Uri messageUri)385     private void storeAddressPart(Bmessage message, Uri messageUri) {
386         ContentValues values = new ContentValues();
387         Uri contentUri = Uri.parse(messageUri.toString() + "/addr");
388         String originator = getOriginatorNumber(message);
389         values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET);
390 
391         values.put(Mms.Addr.ADDRESS, originator);
392         values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE);
393         mResolver.insert(contentUri, values);
394 
395         Set<String> messageContacts = new ArraySet<>();
396         getRecipientsFromMessage(message, messageContacts);
397         for (String recipient : messageContacts) {
398             values.put(Mms.Addr.ADDRESS, recipient);
399             values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE);
400             mResolver.insert(contentUri, values);
401         }
402     }
403 
insertIntoMmsTable(String subject)404     private Uri insertIntoMmsTable(String subject) {
405         ContentValues mmsValues = new ContentValues();
406         mmsValues.put(Mms.TEXT_ONLY, 1);
407         mmsValues.put(Mms.MESSAGE_TYPE, 128);
408         mmsValues.put(Mms.SUBJECT, subject);
409         return mResolver.insert(Mms.CONTENT_URI, mmsValues);
410     }
411 
412     /**
413      * cleanUp
414      * clear the subscription info and content on shutdown
415      */
cleanUp()416     void cleanUp() {
417         mResolver.unregisterContentObserver(mContentObserver);
418         clearMessages(mContext, mSubscriptionId);
419         try {
420             mSubscriptionManager.removeSubscriptionInfoRecord(mDevice.getAddress(),
421                     SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
422         } catch (Exception e) {
423             Log.w(TAG, "cleanUp failed: " + e.toString());
424         }
425     }
426 
427     /**
428      * clearMessages
429      * clean up the content provider on startup
430      */
clearMessages(Context context, int subscriptionId)431     private static void clearMessages(Context context, int subscriptionId) {
432         ContentResolver resolver = context.getContentResolver();
433         String threads = new String();
434 
435         Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
436         Cursor threadCursor = resolver.query(uri, null, null, null, null);
437         while (threadCursor.moveToNext()) {
438             threads += threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID)) + ", ";
439         }
440 
441         resolver.delete(Sms.CONTENT_URI, Sms.SUBSCRIPTION_ID + " =? ",
442                 new String[]{Integer.toString(subscriptionId)});
443         resolver.delete(Mms.CONTENT_URI, Mms.SUBSCRIPTION_ID + " =? ",
444                 new String[]{Integer.toString(subscriptionId)});
445         if (threads.length() > 2) {
446             threads = threads.substring(0, threads.length() - 2);
447             resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null);
448         }
449     }
450 
451     /**
452      * getThreadId
453      * utilize the originator and recipients to obtain the thread id
454      */
getThreadId(Bmessage message)455     private long getThreadId(Bmessage message) {
456 
457         Set<String> messageContacts = new ArraySet<>();
458         String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
459         if (originator != null) {
460             messageContacts.add(originator);
461         }
462         getRecipientsFromMessage(message, messageContacts);
463         // If there is only one contact don't remove it.
464         if (messageContacts.isEmpty()) {
465             return Telephony.Threads.COMMON_THREAD;
466         } else if (messageContacts.size() > 1) {
467             messageContacts.removeIf(number -> (PhoneNumberUtils.areSamePhoneNumber(number,
468                     mPhoneNumber, mTelephonyManager.getNetworkCountryIso())));
469         }
470 
471         logV("Contacts = " + messageContacts.toString());
472         return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts);
473     }
474 
getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)475     private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) {
476         List<VCardEntry> recipients = message.getRecipients();
477         for (VCardEntry recipient : recipients) {
478             List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
479             if (phoneData != null && !phoneData.isEmpty()) {
480                 messageContacts
481                         .add(PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()));
482             }
483         }
484     }
485 
getOriginatorNumber(Bmessage message)486     private String getOriginatorNumber(Bmessage message) {
487         VCardEntry originator = message.getOriginator();
488         if (originator == null) {
489             return null;
490         }
491 
492         List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
493         if (phoneData == null || phoneData.isEmpty()) {
494             return null;
495         }
496 
497         return PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber());
498     }
499 
getFirstRecipientNumber(Bmessage message)500     private String getFirstRecipientNumber(Bmessage message) {
501         List<VCardEntry> recipients = message.getRecipients();
502         if (recipients == null || recipients.isEmpty()) {
503             return null;
504         }
505 
506         List<VCardEntry.PhoneData> phoneData = recipients.get(0).getPhoneList();
507         if (phoneData == null || phoneData.isEmpty()) {
508             return null;
509         }
510 
511         return phoneData.get(0).getNumber();
512     }
513 
514     /**
515      * addThreadContactToEntries
516      * utilizing the thread id fill in the appropriate fields of bmsg with the intended recipients
517      */
addThreadContactsToEntries(Bmessage bmsg, String thread)518     boolean addThreadContactsToEntries(Bmessage bmsg, String thread) {
519         String threadId = Uri.parse(thread).getLastPathSegment();
520 
521         logD("MATCHING THREAD" + threadId);
522         logD(MmsSms.CONTENT_CONVERSATIONS_URI + threadId + "/recipients");
523 
524         Cursor cursor = mResolver
525                 .query(Uri.withAppendedPath(MmsSms.CONTENT_CONVERSATIONS_URI,
526                         threadId + "/recipients"),
527                         null, null,
528                         null, null);
529 
530         if (cursor.moveToNext()) {
531             logD("Columns" + Arrays.toString(cursor.getColumnNames()));
532             logV("CONTACT LIST: " + cursor.getString(cursor.getColumnIndex("recipient_ids")));
533             addRecipientsToEntries(bmsg,
534                     cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" "));
535             return true;
536         } else {
537             Log.w(TAG, "Thread Not Found");
538             return false;
539         }
540     }
541 
542 
addRecipientsToEntries(Bmessage bmsg, String[] recipients)543     private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) {
544         logV("CONTACT LIST: " + Arrays.toString(recipients));
545         for (String recipient : recipients) {
546             Cursor cursor = mResolver
547                     .query(Uri.parse("content://mms-sms/canonical-address/" + recipient), null,
548                             null, null,
549                             null);
550             while (cursor.moveToNext()) {
551                 String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS));
552                 logV("CONTACT number: " + number);
553                 VCardEntry destEntry = new VCardEntry();
554                 VCardProperty destEntryPhone = new VCardProperty();
555                 destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
556                 destEntryPhone.addValues(number);
557                 destEntry.addProperty(destEntryPhone);
558                 bmsg.addRecipient(destEntry);
559             }
560         }
561     }
562 
563     /**
564      * MessageStatus
565      *
566      * Helper class to store associations between remote and local provider based on message handle
567      * and read status
568      */
569     class MessageStatus {
570 
571         String mHandle;
572         int mRead;
573 
MessageStatus(String handle, int read)574         MessageStatus(String handle, int read) {
575             mHandle = handle;
576             mRead = read;
577         }
578 
579         @Override
equals(Object other)580         public boolean equals(Object other) {
581             return ((other instanceof MessageStatus) && ((MessageStatus) other).mHandle
582                     .equals(mHandle));
583         }
584     }
585 }
586