1/************************************************************************************
2 *
3 *  Copyright (C) 2009-2012 Broadcom Corporation
4 *
5 *  Licensed under the Apache License, Version 2.0 (the "License");
6 *  you may not use this file except in compliance with the License.
7 *  You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 *
17 ************************************************************************************/
18package com.android.bluetooth.pbap;
19
20import android.content.Context;
21import android.content.ContentResolver;
22import android.content.res.AssetFileDescriptor;
23import android.content.SharedPreferences;
24import android.content.SharedPreferences.Editor;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Handler;
28import android.preference.PreferenceManager;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.CommonDataKinds.Phone;
31import android.provider.ContactsContract.CommonDataKinds.Email;
32import android.provider.ContactsContract.CommonDataKinds.StructuredName;
33import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
34import android.provider.ContactsContract.Data;
35import android.provider.ContactsContract.RawContacts;
36import android.provider.ContactsContract.Profile;
37import android.provider.ContactsContract.RawContactsEntity;
38
39import android.util.Log;
40
41import com.android.vcard.VCardComposer;
42import com.android.vcard.VCardConfig;
43import com.android.bluetooth.Utils;
44import com.android.bluetooth.pbap.BluetoothPbapService;
45
46import java.io.File;
47import java.io.FileInputStream;
48import java.io.FileOutputStream;
49import java.lang.Math;
50import java.util.ArrayList;
51import java.util.Arrays;
52import java.util.concurrent.atomic.AtomicLong;
53import java.util.Calendar;
54import java.util.HashMap;
55import java.util.HashSet;
56
57public class BluetoothPbapUtils {
58    private static final String TAG = "BluetoothPbapUtils";
59    private static final boolean V = BluetoothPbapService.VERBOSE;
60
61    public static int FILTER_PHOTO = 3;
62    public static int FILTER_TEL = 7;
63    public static int FILTER_NICKNAME = 23;
64    private static final long QUERY_CONTACT_RETRY_INTERVAL = 4000;
65
66    protected static AtomicLong mDbIdentifier = new AtomicLong();
67
68    protected static long primaryVersionCounter = 0;
69    protected static long secondaryVersionCounter = 0;
70    public static long totalContacts = 0;
71
72    /* totalFields and totalSvcFields used to update primary/secondary version
73     * counter between pbap sessions*/
74    public static long totalFields = 0;
75    public static long totalSvcFields = 0;
76    public static long contactsLastUpdated = 0;
77    public static boolean contactsLoaded = false;
78
79    private static class ContactData {
80        private String name;
81        private ArrayList<String> email;
82        private ArrayList<String> phone;
83        private ArrayList<String> address;
84
85        public ContactData() {
86            phone = new ArrayList<String>();
87            email = new ArrayList<String>();
88            address = new ArrayList<String>();
89        }
90
91        public ContactData(String name, ArrayList<String> phone, ArrayList<String> email,
92                ArrayList<String> address) {
93            this.name = name;
94            this.phone = phone;
95            this.email = email;
96            this.address = address;
97        }
98    }
99
100    private static HashMap<String, ContactData> contactDataset = new HashMap<String, ContactData>();
101
102    private static HashSet<String> ContactSet = new HashSet<String>();
103
104    private static final String TYPE_NAME = "name";
105    private static final String TYPE_PHONE = "phone";
106    private static final String TYPE_EMAIL = "email";
107    private static final String TYPE_ADDRESS = "address";
108
109    public static boolean hasFilter(byte[] filter) {
110        return filter != null && filter.length > 0;
111    }
112
113    public static boolean isNameAndNumberOnly(byte[] filter) {
114        // For vcard 2.0: VERSION,N,TEL is mandatory
115        // For vcard 3.0, VERSION,N,FN,TEL is mandatory
116        // So we only need to make sure that no other fields except optionally
117        // NICKNAME is set
118
119        // Check that an explicit filter is not set. If not, this means
120        // return everything
121        if (!hasFilter(filter)) {
122            Log.v(TAG, "No filter set. isNameAndNumberOnly=false");
123            return false;
124        }
125
126        // Check bytes 0-4 are all 0
127        for (int i = 0; i <= 4; i++) {
128            if (filter[i] != 0) {
129                return false;
130            }
131        }
132        // On byte 5, only BIT_NICKNAME can be set, so make sure
133        // rest of bits are not set
134        if ((filter[5] & 0x7F) > 0) {
135            return false;
136        }
137
138        // Check byte 6 is not set
139        if (filter[6] != 0) {
140            return false;
141        }
142
143        // Check if bit#3-6 is set. Return false if so.
144        if ((filter[7] & 0x78) > 0) {
145            return false;
146        }
147
148        return true;
149    }
150
151    public static boolean isFilterBitSet(byte[] filter, int filterBit) {
152        if (hasFilter(filter)) {
153            int byteNumber = 7 - filterBit / 8;
154            int bitNumber = filterBit % 8;
155            if (byteNumber < filter.length) {
156                return (filter[byteNumber] & (1 << bitNumber)) > 0;
157            }
158        }
159        return false;
160    }
161
162    public static VCardComposer createFilteredVCardComposer(final Context ctx,
163            final int vcardType, final byte[] filter) {
164        int vType = vcardType;
165        boolean includePhoto = BluetoothPbapConfig.includePhotosInVcard()
166                    && (!hasFilter(filter) || isFilterBitSet(filter, FILTER_PHOTO));
167        if (!includePhoto) {
168            if (V) Log.v(TAG, "Excluding images from VCardComposer...");
169            vType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
170        }
171        return new VCardComposer(ctx, vType, true);
172    }
173
174    public static boolean isProfileSet(Context context) {
175        Cursor c = context.getContentResolver().query(
176                Profile.CONTENT_VCARD_URI, new String[] { Profile._ID }, null,
177                null, null);
178        boolean isSet = (c != null && c.getCount() > 0);
179        if (c != null) {
180            c.close();
181            c = null;
182        }
183        return isSet;
184    }
185
186    public static String getProfileName(Context context) {
187        Cursor c = context.getContentResolver().query(
188                Profile.CONTENT_URI, new String[] { Profile.DISPLAY_NAME}, null,
189                null, null);
190        String ownerName =null;
191        if (c!= null && c.moveToFirst()) {
192            ownerName = c.getString(0);
193        }
194        if (c != null) {
195            c.close();
196            c = null;
197        }
198        return ownerName;
199    }
200    public static final String createProfileVCard(Context ctx, final int vcardType,final byte[] filter) {
201        VCardComposer composer = null;
202        String vcard = null;
203        try {
204            composer = createFilteredVCardComposer(ctx, vcardType, filter);
205            if (composer
206                    .init(Profile.CONTENT_URI, null, null, null, null, Uri
207                            .withAppendedPath(Profile.CONTENT_URI,
208                                    RawContactsEntity.CONTENT_URI
209                                            .getLastPathSegment()))) {
210                vcard = composer.createOneEntry();
211            } else {
212                Log.e(TAG,
213                        "Unable to create profile vcard. Error initializing composer: "
214                                + composer.getErrorReason());
215            }
216        } catch (Throwable t) {
217            Log.e(TAG, "Unable to create profile vcard.", t);
218        }
219        if (composer != null) {
220            try {
221                composer.terminate();
222            } catch (Throwable t) {
223
224            }
225        }
226        return vcard;
227    }
228
229    public static boolean createProfileVCardFile(File file, Context context) {
230        FileInputStream is = null;
231        FileOutputStream os = null;
232        boolean success = true;
233        try {
234            AssetFileDescriptor fd = context.getContentResolver()
235                    .openAssetFileDescriptor(Profile.CONTENT_VCARD_URI, "r");
236
237            if(fd == null)
238            {
239                return false;
240            }
241            is = fd.createInputStream();
242            os = new FileOutputStream(file);
243            Utils.copyStream(is, os, 200);
244        } catch (Throwable t) {
245            Log.e(TAG, "Unable to create default contact vcard file", t);
246            success = false;
247        }
248        Utils.safeCloseStream(is);
249        Utils.safeCloseStream(os);
250        return success;
251    }
252
253    protected static void savePbapParams(Context ctx, long primaryCounter, long secondaryCounter,
254            long dbIdentifier, long lastUpdatedTimestamp, long totalFields, long totalSvcFields,
255            long totalContacts) {
256        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
257        Editor edit = pref.edit();
258        edit.putLong("primary", primaryCounter);
259        edit.putLong("secondary", secondaryCounter);
260        edit.putLong("dbIdentifier", dbIdentifier);
261        edit.putLong("totalContacts", totalContacts);
262        edit.putLong("lastUpdatedTimestamp", lastUpdatedTimestamp);
263        edit.putLong("totalFields", totalFields);
264        edit.putLong("totalSvcFields", totalSvcFields);
265        edit.apply();
266
267        if (V)
268            Log.v(TAG, "Saved Primary:" + primaryCounter + ", Secondary:" + secondaryCounter
269                            + ", Database Identifier: " + dbIdentifier);
270    }
271
272    /* fetchPbapParams() loads preserved value of Database Identifiers and folder
273     * version counters. Servers using a database identifier 0 or regenerating
274     * one at each connection will not benefit from the resulting performance and
275     * user experience improvements. So database identifier is set with current
276     * timestamp and updated on rollover of folder version counter.*/
277    protected static void fetchPbapParams(Context ctx) {
278        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
279        long timeStamp = Calendar.getInstance().getTimeInMillis();
280        BluetoothPbapUtils.mDbIdentifier.set(pref.getLong("mDbIdentifier", timeStamp));
281        BluetoothPbapUtils.primaryVersionCounter = pref.getLong("primary", 0);
282        BluetoothPbapUtils.secondaryVersionCounter = pref.getLong("secondary", 0);
283        BluetoothPbapUtils.totalFields = pref.getLong("totalContacts", 0);
284        BluetoothPbapUtils.contactsLastUpdated = pref.getLong("lastUpdatedTimestamp", timeStamp);
285        BluetoothPbapUtils.totalFields = pref.getLong("totalFields", 0);
286        BluetoothPbapUtils.totalSvcFields = pref.getLong("totalSvcFields", 0);
287        if (V) Log.v(TAG, " fetchPbapParams " + pref.getAll());
288    }
289
290    /* loadAllContacts() fetches data like name,phone,email or addrees related to
291     * all contacts. It is required to determine which field of the contact is
292     * added/updated/deleted to increment secondary version counter accordingly.*/
293    protected static void loadAllContacts(Context mContext, Handler mHandler) {
294        if (V) Log.v(TAG, "Loading Contacts ...");
295
296        try {
297            String[] projection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
298            int contactCount = 0;
299            if ((contactCount = fetchAndSetContacts(
300                         mContext, mHandler, projection, null, null, true))
301                    < 0)
302                return;
303            totalContacts = contactCount; // to set total contacts count fetched on Connect
304            contactsLoaded = true;
305        } catch (Exception e) {
306            Log.e(TAG, "Exception occurred in load contacts: " + e);
307        }
308    }
309
310    protected static void updateSecondaryVersionCounter(Context mContext, Handler mHandler) {
311        try {
312            /* updated_list stores list of contacts which are added/updated after
313             * the time when contacts were last updated. (contactsLastUpdated
314             * indicates the time when contact/contacts were last updated and
315             * corresponding changes were reflected in Folder Version Counters).*/
316            ArrayList<String> updated_list = new ArrayList<String>();
317            HashSet<String> currentContactSet = new HashSet<String>();
318            int currentContactCount = 0;
319
320            String[] projection = {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP};
321            Cursor c = mContext.getContentResolver().query(
322                    Contacts.CONTENT_URI, projection, null, null, null);
323
324            if (c == null) {
325                Log.d(TAG, "Failed to fetch data from contact database");
326                return;
327            }
328            while (c.moveToNext()) {
329                String contactId = c.getString(0);
330                long lastUpdatedTime = c.getLong(1);
331                if (lastUpdatedTime > contactsLastUpdated) {
332                    updated_list.add(contactId);
333                }
334                currentContactSet.add(contactId);
335            }
336            currentContactCount = c.getCount();
337            c.close();
338
339            if (V) Log.v(TAG, "updated list =" + updated_list);
340            String[] dataProjection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
341
342            String whereClause = Data.CONTACT_ID + "=?";
343
344            /* code to check if new contact/contacts are added */
345            if (currentContactCount > totalContacts) {
346                for (int i = 0; i < updated_list.size(); i++) {
347                    String[] selectionArgs = {updated_list.get(i)};
348                    fetchAndSetContacts(
349                            mContext, mHandler, dataProjection, whereClause, selectionArgs, false);
350                    secondaryVersionCounter++;
351                    primaryVersionCounter++;
352                    totalContacts = currentContactCount;
353                }
354                /* When contact/contacts are deleted */
355            } else if (currentContactCount < totalContacts) {
356                totalContacts = currentContactCount;
357                ArrayList<String> svcFields = new ArrayList<String>(
358                        Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE,
359                                Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE));
360                HashSet<String> deletedContacts = new HashSet<String>(ContactSet);
361                deletedContacts.removeAll(currentContactSet);
362                primaryVersionCounter += deletedContacts.size();
363                secondaryVersionCounter += deletedContacts.size();
364                if (V) Log.v(TAG, "Deleted Contacts : " + deletedContacts);
365
366                // to decrement totalFields and totalSvcFields count
367                for (String deletedContact : deletedContacts) {
368                    ContactSet.remove(deletedContact);
369                    String[] selectionArgs = {deletedContact};
370                    Cursor dataCursor = mContext.getContentResolver().query(
371                            Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
372
373                    if (dataCursor == null) {
374                        Log.d(TAG, "Failed to fetch data from contact database");
375                        return;
376                    }
377
378                    while (dataCursor.moveToNext()) {
379                        if (svcFields.contains(
380                                    dataCursor.getString(dataCursor.getColumnIndex(Data.MIMETYPE))))
381                            totalSvcFields--;
382                        totalFields--;
383                    }
384                    dataCursor.close();
385                }
386
387                /* When contacts are updated. i.e. Fields of existing contacts are
388                 * added/updated/deleted */
389            } else {
390                for (int i = 0; i < updated_list.size(); i++) {
391                    primaryVersionCounter++;
392                    ArrayList<String> phone_tmp = new ArrayList<String>();
393                    ArrayList<String> email_tmp = new ArrayList<String>();
394                    ArrayList<String> address_tmp = new ArrayList<String>();
395                    String name_tmp = null, updatedCID = updated_list.get(i);
396                    boolean updated = false;
397
398                    String[] selectionArgs = {updated_list.get(i)};
399                    Cursor dataCursor = mContext.getContentResolver().query(
400                            Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
401
402                    if (dataCursor == null) {
403                        Log.d(TAG, "Failed to fetch data from contact database");
404                        return;
405                    }
406                    // fetch all updated contacts and compare with cached copy of contacts
407                    int indexData = dataCursor.getColumnIndex(Data.DATA1);
408                    int indexMimeType = dataCursor.getColumnIndex(Data.MIMETYPE);
409                    String data;
410                    String mimeType;
411                    while (dataCursor.moveToNext()) {
412                        data = dataCursor.getString(indexData);
413                        mimeType = dataCursor.getString(indexMimeType);
414                        switch (mimeType) {
415                            case Email.CONTENT_ITEM_TYPE:
416                                email_tmp.add(data);
417                                break;
418                            case Phone.CONTENT_ITEM_TYPE:
419                                phone_tmp.add(data);
420                                break;
421                            case StructuredPostal.CONTENT_ITEM_TYPE:
422                                address_tmp.add(data);
423                                break;
424                            case StructuredName.CONTENT_ITEM_TYPE:
425                                name_tmp = new String(data);
426                                break;
427                        }
428                    }
429                    ContactData cData =
430                            new ContactData(name_tmp, phone_tmp, email_tmp, address_tmp);
431                    dataCursor.close();
432
433                    if ((name_tmp == null && contactDataset.get(updatedCID).name != null)
434                            || (name_tmp != null && contactDataset.get(updatedCID).name == null)
435                            || (!(name_tmp == null && contactDataset.get(updatedCID).name == null)
436                                       && !name_tmp.equals(contactDataset.get(updatedCID).name))) {
437                        updated = true;
438                    } else if (checkFieldUpdates(contactDataset.get(updatedCID).phone, phone_tmp)) {
439                        updated = true;
440                    } else if (checkFieldUpdates(contactDataset.get(updatedCID).email, email_tmp)) {
441                        updated = true;
442                    } else if (checkFieldUpdates(
443                                       contactDataset.get(updatedCID).address, address_tmp)) {
444                        updated = true;
445                    }
446
447                    if (updated) {
448                        secondaryVersionCounter++;
449                        contactDataset.put(updatedCID, cData);
450                    }
451                }
452            }
453
454            Log.d(TAG, "primaryVersionCounter = " + primaryVersionCounter
455                            + ", secondaryVersionCounter=" + secondaryVersionCounter);
456
457            // check if Primary/Secondary version Counter has rolled over
458            if (secondaryVersionCounter < 0 || primaryVersionCounter < 0)
459                mHandler.sendMessage(
460                        mHandler.obtainMessage(BluetoothPbapService.ROLLOVER_COUNTERS));
461        } catch (Exception e) {
462            Log.e(TAG, "Exception while updating secondary version counter:" + e);
463        }
464    }
465
466    /* checkFieldUpdates checks update contact fields of a particular contact.
467     * Field update can be a field updated/added/deleted in an existing contact.
468     * Returns true if any contact field is updated else return false. */
469    protected static boolean checkFieldUpdates(
470            ArrayList<String> oldFields, ArrayList<String> newFields) {
471        if (newFields != null && oldFields != null) {
472            if (newFields.size() != oldFields.size()) {
473                totalSvcFields += Math.abs(newFields.size() - oldFields.size());
474                totalFields += Math.abs(newFields.size() - oldFields.size());
475                return true;
476            }
477            for (int i = 0; i < newFields.size(); i++) {
478                if (!oldFields.contains(newFields.get(i))) {
479                    return true;
480                }
481            }
482            /* when all fields of type(phone/email/address) are deleted in a given contact*/
483        } else if (newFields == null && oldFields != null && oldFields.size() > 0) {
484            totalSvcFields += oldFields.size();
485            totalFields += oldFields.size();
486            return true;
487
488            /* when new fields are added for a type(phone/email/address) in a contact
489             * for which there were no fields of this type earliar.*/
490        } else if (oldFields == null && newFields != null && newFields.size() > 0) {
491            totalSvcFields += newFields.size();
492            totalFields += newFields.size();
493            return true;
494        }
495        return false;
496    }
497
498    /* fetchAndSetContacts reads contacts and caches them
499     * isLoad = true indicates its loading all contacts
500     * isLoad = false indiacates its caching recently added contact in database*/
501    protected static int fetchAndSetContacts(Context mContext, Handler mHandler,
502            String[] projection, String whereClause, String[] selectionArgs, boolean isLoad) {
503        long currentTotalFields = 0, currentSvcFieldCount = 0;
504        Cursor c = mContext.getContentResolver().query(
505                Data.CONTENT_URI, projection, whereClause, selectionArgs, null);
506
507        /* send delayed message to loadContact when ContentResolver is unable
508         * to fetch data from contact database using the specified URI at that
509         * moment (Case: immediate Pbap connect on system boot with BT ON)*/
510        if (c == null) {
511            Log.d(TAG, "Failed to fetch contacts data from database..");
512            if (isLoad)
513                mHandler.sendMessageDelayed(
514                        mHandler.obtainMessage(BluetoothPbapService.LOAD_CONTACTS),
515                        QUERY_CONTACT_RETRY_INTERVAL);
516            return -1;
517        }
518
519        int indexCId = c.getColumnIndex(Data.CONTACT_ID);
520        int indexData = c.getColumnIndex(Data.DATA1);
521        int indexMimeType = c.getColumnIndex(Data.MIMETYPE);
522        String contactId, data, mimeType;
523        while (c.moveToNext()) {
524            contactId = c.getString(indexCId);
525            data = c.getString(indexData);
526            mimeType = c.getString(indexMimeType);
527            /* fetch phone/email/address/name information of the contact */
528            switch (mimeType) {
529                case Phone.CONTENT_ITEM_TYPE:
530                    setContactFields(TYPE_PHONE, contactId, data);
531                    currentSvcFieldCount++;
532                    break;
533                case Email.CONTENT_ITEM_TYPE:
534                    setContactFields(TYPE_EMAIL, contactId, data);
535                    currentSvcFieldCount++;
536                    break;
537                case StructuredPostal.CONTENT_ITEM_TYPE:
538                    setContactFields(TYPE_ADDRESS, contactId, data);
539                    currentSvcFieldCount++;
540                    break;
541                case StructuredName.CONTENT_ITEM_TYPE:
542                    setContactFields(TYPE_NAME, contactId, data);
543                    currentSvcFieldCount++;
544                    break;
545            }
546            ContactSet.add(contactId);
547            currentTotalFields++;
548        }
549        c.close();
550
551        /* This code checks if there is any update in contacts after last pbap
552         * disconnect has happenned (even if BT is turned OFF during this time)*/
553        if (isLoad && currentTotalFields != totalFields) {
554            primaryVersionCounter += Math.abs(totalContacts - ContactSet.size());
555
556            if (currentSvcFieldCount != totalSvcFields)
557                if (totalContacts != ContactSet.size())
558                    secondaryVersionCounter += Math.abs(totalContacts - ContactSet.size());
559                else
560                    secondaryVersionCounter++;
561            if (primaryVersionCounter < 0 || secondaryVersionCounter < 0) rolloverCounters();
562
563            totalFields = currentTotalFields;
564            totalSvcFields = currentSvcFieldCount;
565            contactsLastUpdated = System.currentTimeMillis();
566            Log.d(TAG, "Contacts updated between last BT OFF and current"
567                            + "Pbap Connect, primaryVersionCounter=" + primaryVersionCounter
568                            + ", secondaryVersionCounter=" + secondaryVersionCounter);
569        } else if (!isLoad) {
570            totalFields++;
571            totalSvcFields++;
572        }
573        return ContactSet.size();
574    }
575
576    /* setContactFields() is used to store contacts data in local cache (phone,
577     * email or address which is required for updating Secondary Version counter).
578     * contactsFieldData - List of field data for phone/email/address.
579     * contactId - Contact ID, data1 - field value from data table for phone/email/address*/
580
581    protected static void setContactFields(String fieldType, String contactId, String data) {
582        ContactData cData = null;
583        if (contactDataset.containsKey(contactId))
584            cData = contactDataset.get(contactId);
585        else
586            cData = new ContactData();
587
588        switch (fieldType) {
589            case TYPE_NAME:
590                cData.name = data;
591                break;
592            case TYPE_PHONE:
593                cData.phone.add(data);
594                break;
595            case TYPE_EMAIL:
596                cData.email.add(data);
597                break;
598            case TYPE_ADDRESS:
599                cData.address.add(data);
600                break;
601        }
602        contactDataset.put(contactId, cData);
603    }
604
605    /* As per Pbap 1.2 specification, Database Identifies shall be
606     * re-generated when a Folder Version Counter rolls over or starts over.*/
607
608    protected static void rolloverCounters() {
609        mDbIdentifier.set(Calendar.getInstance().getTimeInMillis());
610        primaryVersionCounter = (primaryVersionCounter < 0) ? 0 : primaryVersionCounter;
611        secondaryVersionCounter = (secondaryVersionCounter < 0) ? 0 : secondaryVersionCounter;
612        if (V) Log.v(TAG, "mDbIdentifier rolled over to:" + mDbIdentifier);
613    }
614}
615