1/*
2* Copyright (C) 2015 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15
16package com.android.bluetooth.mapapi;
17
18import android.content.ContentProvider;
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.UriMatcher;
23import android.content.pm.ProviderInfo;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.os.Binder;
28import android.os.Bundle;
29import android.os.ParcelFileDescriptor;
30import android.util.Log;
31
32import java.io.FileInputStream;
33import java.io.FileNotFoundException;
34import java.io.FileOutputStream;
35import java.io.IOException;
36import java.util.List;
37import java.util.Map;
38import java.util.Map.Entry;
39import java.util.Set;
40
41/**
42 * A base implementation of the BluetoothMapContract.
43 * A base class for a ContentProvider that allows access to Instant messages from a Bluetooth
44 * device through the Message Access Profile.
45 */
46public abstract class BluetoothMapIMProvider extends ContentProvider {
47
48    private static final String TAG = "BluetoothMapIMProvider";
49    private static final boolean D = true;
50
51    private static final int MATCH_ACCOUNT = 1;
52    private static final int MATCH_MESSAGE = 3;
53    private static final int MATCH_CONVERSATION = 4;
54    private static final int MATCH_CONVOCONTACT = 5;
55
56    protected ContentResolver mResolver;
57
58    private Uri CONTENT_URI = null;
59    private String mAuthority;
60    private UriMatcher mMatcher;
61
62    /**
63     * @return the CONTENT_URI exposed. This will be used to send out notifications.
64     */
65    abstract protected Uri getContentUri();
66
67    /**
68     * Implementation is provided by the parent class.
69     */
70    @Override
71    public void attachInfo(Context context, ProviderInfo info) {
72       mAuthority = info.authority;
73
74       mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
75       mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT);
76       mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE);
77       mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVERSATION,
78               MATCH_CONVERSATION);
79       mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVOCONTACT,
80               MATCH_CONVOCONTACT);
81
82       // Sanity check our setup
83       if (!info.exported) {
84           throw new SecurityException("Provider must be exported");
85       }
86       // Enforce correct permissions are used
87       if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)){
88           throw new SecurityException("Provider must be protected by " +
89                   android.Manifest.permission.BLUETOOTH_MAP);
90       }
91       if(D) Log.d(TAG,"attachInfo() mAuthority = " + mAuthority);
92
93       mResolver = context.getContentResolver();
94       super.attachInfo(context, info);
95   }
96
97    /**
98     * This function shall be called when any Account database content have changed
99     * to Notify any attached observers.
100     * @param accountId the ID of the account that changed. Null is a valid value,
101     *        if accountId is unknown or multiple accounts changed.
102     */
103    protected void onAccountChanged(String accountId) {
104        Uri newUri = null;
105
106        if(mAuthority == null){
107            return;
108        }
109        if(accountId == null){
110            newUri = BluetoothMapContract.buildAccountUri(mAuthority);
111        } else {
112            newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId);
113        }
114
115        if(D) Log.d(TAG,"onAccountChanged() accountId = " + accountId + " URI: " + newUri);
116        mResolver.notifyChange(newUri, null);
117    }
118
119    /**
120     * This function shall be called when any Message database content have changed
121     * to notify any attached observers.
122     * @param accountId Null is a valid value, if accountId is unknown, but
123     *        recommended for increased performance.
124     * @param messageId Null is a valid value, if multiple messages changed or the
125     *        messageId is unknown, but recommended for increased performance.
126     */
127    protected void onMessageChanged(String accountId, String messageId) {
128        Uri newUri = null;
129
130        if(mAuthority == null){
131            return;
132        }
133        if(accountId == null){
134            newUri = BluetoothMapContract.buildMessageUri(mAuthority);
135        } else {
136            if(messageId == null)
137            {
138                newUri = BluetoothMapContract.buildMessageUri(mAuthority,accountId);
139            } else {
140                newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority,accountId,
141                        messageId);
142            }
143        }
144        if(D) Log.d(TAG,"onMessageChanged() accountId = " + accountId
145                + " messageId = " + messageId + " URI: " + newUri);
146        mResolver.notifyChange(newUri, null);
147    }
148
149
150    /**
151     * This function shall be called when any Message database content have changed
152     * to notify any attached observers.
153     * @param accountId Null is a valid value, if accountId is unknown, but
154     *        recommended for increased performance.
155     * @param contactId Null is a valid value, if multiple contacts changed or the
156     *        contactId is unknown, but recommended for increased performance.
157     */
158    protected void onContactChanged(String accountId, String contactId) {
159        Uri newUri = null;
160
161        if(mAuthority == null){
162            return;
163        }
164        if(accountId == null){
165            newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority);
166        } else {
167            if(contactId == null)
168            {
169                newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority,accountId);
170            } else {
171                newUri = BluetoothMapContract.buildConvoContactsUriWithId(mAuthority, accountId,
172                        contactId);
173            }
174        }
175        if(D) Log.d(TAG,"onContactChanged() accountId = " + accountId
176                + " contactId = " + contactId + " URI: " + newUri);
177        mResolver.notifyChange(newUri, null);
178    }
179
180    /**
181     * Not used, this is just a dummy implementation.
182     * TODO: We might need to something intelligent here after introducing IM
183     */
184    @Override
185    public String getType(Uri uri) {
186        return "InstantMessage";
187    }
188
189    /**
190     * The MAP specification states that a delete request from MAP client is a folder shift to the
191     * 'deleted' folder.
192     * Only use case of delete() is when transparency is requested for push messages, then
193     * message should not remain in sent folder and therefore must be deleted
194     */
195    @Override
196    public int delete(Uri uri, String where, String[] selectionArgs) {
197        if (D) Log.d(TAG, "delete(): uri=" + uri.toString() );
198        int result = 0;
199
200        String table = uri.getPathSegments().get(1);
201        if(table == null)
202            throw new IllegalArgumentException("Table missing in URI");
203        // the id of the entry to be deleted from the database
204        String messageId = uri.getLastPathSegment();
205        if (messageId == null)
206            throw new IllegalArgumentException("Message ID missing in update values!");
207
208        String accountId = getAccountId(uri);
209        if (accountId == null)
210            throw new IllegalArgumentException("Account ID missing in update values!");
211
212        long callingId = Binder.clearCallingIdentity();
213        try {
214            if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
215                return deleteMessage(accountId, messageId);
216            } else {
217                if (D) Log.w(TAG, "Unknown table name: " + table);
218                return result;
219            }
220        } finally {
221            Binder.restoreCallingIdentity(callingId);
222        }
223    }
224
225    /**
226     * This function deletes a message.
227     * @param accountId the ID of the Account
228     * @param messageId the ID of the message to delete.
229     * @return the number of messages deleted - 0 if the message was not found.
230     */
231    abstract protected int deleteMessage(String accountId, String messageId);
232
233    /**
234     * Insert is used to add new messages to the data base.
235     * Insert message approach:
236     *   - Insert an empty message to get an _id with only a folder_id
237     *   - Open the _id for write
238     *   - Write the message content
239     *     (When the writer completes, this provider should do an update of the message)
240     */
241    @Override
242    public Uri insert(Uri uri, ContentValues values) {
243        String table = uri.getLastPathSegment();
244        if(table == null)
245            throw new IllegalArgumentException("Table missing in URI");
246
247        String accountId = getAccountId(uri);
248        if (accountId == null)
249            throw new IllegalArgumentException("Account ID missing in URI");
250
251        // TODO: validate values?
252
253        String id; // the id of the entry inserted into the database
254        long callingId = Binder.clearCallingIdentity();
255        Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " +
256                uri.getLastPathSegment());
257        try {
258            if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
259                id = insertMessage(accountId, values);
260                if(D) Log.i(TAG, "insert() ID: " + id);
261                return Uri.parse(uri.toString() + "/" + id);
262            } else {
263                Log.w(TAG, "Unknown table name: " + table);
264                return null;
265            }
266        } finally {
267            Binder.restoreCallingIdentity(callingId);
268        }
269    }
270
271
272    /**
273     * Inserts an empty message into the Message data base in the specified folder.
274     * This is done before the actual message content is written by fileIO.
275     * @param accountId the ID of the account
276     * @param folderId the ID of the folder to create a new message in.
277     * @return the message id as a string
278     */
279    abstract protected String insertMessage(String accountId, ContentValues values);
280
281     /**
282     * Utility function to build a projection based on a projectionMap.
283     *
284     *   "btColumnName" -> "imColumnName as btColumnName" for each entry.
285     *
286     * This supports SQL statements in the column name entry.
287     * @param projection
288     * @param projectionMap <string, string>
289     * @return the converted projection
290     */
291    protected String[] convertProjection(String[] projection, Map<String,String> projectionMap) {
292        String[] newProjection = new String[projection.length];
293        for(int i = 0; i < projection.length; i++) {
294            newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i];
295        }
296        return newProjection;
297    }
298
299    /**
300     * This query needs to map from the data used in the e-mail client to
301     * BluetoothMapContract type of data.
302     */
303    @Override
304    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
305            String sortOrder) {
306        long callingId = Binder.clearCallingIdentity();
307        try {
308            String accountId = null;
309            if(D)Log.w(TAG, "query(): uri =" + mAuthority + " uri=" + uri.toString());
310
311            switch (mMatcher.match(uri)) {
312                case MATCH_ACCOUNT:
313                    return queryAccount(projection, selection, selectionArgs, sortOrder);
314                case MATCH_MESSAGE:
315                    // TODO: Extract account from URI
316                    accountId = getAccountId(uri);
317                    return queryMessage(accountId, projection, selection, selectionArgs, sortOrder);
318                case MATCH_CONVERSATION:
319                    accountId = getAccountId(uri);
320                    String value;
321                    String searchString =
322                            uri.getQueryParameter(BluetoothMapContract.FILTER_ORIGINATOR_SUBSTRING);
323                    Long periodBegin = null;
324                    value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_BEGIN);
325                    if(value != null) {
326                        periodBegin = Long.parseLong(value);
327                    }
328                    Long periodEnd = null;
329                    value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_END);
330                    if(value != null) {
331                        periodEnd = Long.parseLong(value);
332                    }
333                    Boolean read = null;
334                    value = uri.getQueryParameter(BluetoothMapContract.FILTER_READ_STATUS);
335                    if(value != null) {
336                        read = value.equalsIgnoreCase("true");
337                    }
338                    Long threadId = null;
339                    value = uri.getQueryParameter(BluetoothMapContract.FILTER_THREAD_ID);
340                    if(value != null) {
341                        threadId = Long.parseLong(value);
342                    }
343                    return queryConversation(accountId, threadId, read, periodEnd, periodBegin,
344                            searchString, projection, sortOrder);
345                case MATCH_CONVOCONTACT:
346                    accountId = getAccountId(uri);
347                    long contactId = 0;
348                    return queryConvoContact(accountId, contactId, projection,
349                            selection, selectionArgs, sortOrder);
350                default:
351                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
352            }
353        } finally {
354            Binder.restoreCallingIdentity(callingId);
355        }
356    }
357
358    /**
359     * Query account information.
360     * This function shall return only exposable e-mail accounts. Hence shall not
361     * return accounts that has policies suggesting not to be shared them.
362     * @param projection
363     * @param selection
364     * @param selectionArgs
365     * @param sortOrder
366     * @return a cursor to the accounts that are subject to exposure over BT.
367     */
368    abstract protected Cursor queryAccount(String[] projection, String selection,
369            String[] selectionArgs, String sortOrder);
370
371    /**
372     * For the message table the selection (where clause) can only include the following columns:
373     *    date: less than, greater than and equals
374     *    flagRead: = 1 or = 0
375     *    flagPriority: = 1 or = 0
376     *    folder_id: the ID of the folder only equals
377     *    toList: partial name/address search
378     *    fromList: partial name/address search
379     * Additionally the COUNT and OFFSET shall be supported.
380     * @param accountId the ID of the account
381     * @param projection
382     * @param selection
383     * @param selectionArgs
384     * @param sortOrder
385     * @return a cursor to query result
386     */
387    abstract protected Cursor queryMessage(String accountId, String[] projection, String selection,
388            String[] selectionArgs, String sortOrder);
389
390    /**
391     * For the Conversation table the selection (where clause) can only include
392     * the following columns:
393     *    _id: the ID of the conversation only equals
394     *    name: partial name search
395     *    last_activity: less than, greater than and equals
396     *    version_counter: updated IDs are regenerated
397     * Additionally the COUNT and OFFSET shall be supported.
398     * @param accountId the ID of the account
399     * @param threadId the ID of the conversation
400     * @param projection
401     * @param selection
402     * @param selectionArgs
403     * @param sortOrder
404     * @return a cursor to query result
405     */
406//    abstract protected Cursor queryConversation(Long threadId, String[] projection,
407//    String selection, String[] selectionArgs, String sortOrder);
408
409    /**
410     * Query for conversations with contact information. The expected result is a cursor pointing
411     * to one row for each contact in a conversation.
412     * E.g.:
413     * ThreadId | ThreadName | ... | ContactName | ContactPrecence | ... |
414     *        1 |  "Bowling" | ... |        Hans |               1 | ... |
415     *        1 |  "Bowling" | ... |       Peter |               2 | ... |
416     *        2 |         "" | ... |       Peter |               2 | ... |
417     *        3 |         "" | ... |        Hans |               1 | ... |
418     *
419    * @param accountId the ID of the account
420     * @param threadId filter on a single threadId - null if no filtering is needed.
421     * @param read filter on a read status:
422     *             null: no filtering on read is needed.
423     *             true: return only threads that has NO unread messages.
424     *             false: return only threads that has unread messages.
425     * @param periodEnd   last_activity time stamp of the the newest thread to include in the
426     *                    result.
427     * @param periodBegin last_activity time stamp of the the oldest thread to include in the
428     *                    result.
429     * @param searchString if not null, include only threads that has contacts that matches the
430     *                     searchString as part of the contact name or nickName.
431     * @param projection A list of the columns that is needed in the result
432     * @param sortOrder  the sort order
433     * @return a Cursor representing the query result.
434     */
435    abstract protected Cursor queryConversation(String accountId, Long threadId, Boolean read,
436            Long periodEnd, Long periodBegin, String searchString, String[] projection,
437            String sortOrder);
438
439    /**
440     * For the ConvoContact table the selection (where clause) can only include the
441     * following columns:
442     *    _id: the ID of the contact only equals
443     *    convo_id: id of conversation contact is part of
444     *    name: partial name search
445     *    x_bt_uid: the ID of the bt uid only equals
446     *    chat_state: active, inactive, gone, composing, paused
447     *    last_active: less than, greater than and equals
448     *    presence_state: online, do_not_disturb, away, offline
449     *    priority: level of priority 0 - 100
450     *    last_online: less than, greater than and equals
451     * @param accountId the ID of the account
452     * @param contactId the ID of the contact
453     * @param projection
454     * @param selection
455     * @param selectionArgs
456     * @param sortOrder
457     * @return a cursor to query result
458     */
459    abstract protected Cursor queryConvoContact(String accountId, Long contactId,
460            String[] projection, String selection, String[] selectionArgs, String sortOrder);
461
462    /**
463     * update()
464     * Messages can be modified in the following cases:
465     *  - the folder_key of a message - hence the message can be moved to a new folder,
466     *                                  but the content cannot be modified.
467     *  - the FLAG_READ state can be changed.
468     * Conversations can be modified in the following cases:
469     *  - the read status - changing between read, unread
470     *  - the last activity - the time stamp of last message sent of received in the conversation
471     * ConvoContacts can be modified in the following cases:
472     *  - the chat_state - chat status of the contact in conversation
473     *  - the last_active - the time stamp of last action in the conversation
474     *  - the presence_state - the time stamp of last time contact online
475     *  - the status - the status text of the contact available in a conversation
476     *  - the last_online - the time stamp of last time contact online
477     * The selection statement will always be selection of a message ID, when updating a message,
478     * hence this function will be called multiple times if multiple messages must be updated
479     * due to the nature of the Bluetooth Message Access profile.
480     */
481    @Override
482    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
483
484        String table = uri.getLastPathSegment();
485        if(table == null){
486            throw new IllegalArgumentException("Table missing in URI");
487        }
488        if(selection != null) {
489            throw new IllegalArgumentException("selection shall not be used, ContentValues " +
490                    "shall contain the data");
491        }
492
493        long callingId = Binder.clearCallingIdentity();
494        if(D)Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " +
495                uri.getLastPathSegment());
496        try {
497            if(table.equals(BluetoothMapContract.TABLE_ACCOUNT)) {
498                String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID);
499                if(accountId == null) {
500                    throw new IllegalArgumentException("Account ID missing in update values!");
501                }
502                Integer exposeFlag = values.getAsInteger(
503                        BluetoothMapContract.AccountColumns.FLAG_EXPOSE);
504                if(exposeFlag == null){
505                    throw new IllegalArgumentException("Expose flag missing in update values!");
506                }
507                return updateAccount(accountId, exposeFlag);
508            } else if(table.equals(BluetoothMapContract.TABLE_FOLDER)) {
509                return 0; // We do not support changing folders
510            } else if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
511                String accountId = getAccountId(uri);
512                if(accountId == null) {
513                    throw new IllegalArgumentException("Account ID missing in update values!");
514                }
515                Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID);
516                if(messageId == null) {
517                    throw new IllegalArgumentException("Message ID missing in update values!");
518                }
519                Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID);
520                Boolean flagRead = values.getAsBoolean(
521                        BluetoothMapContract.MessageColumns.FLAG_READ);
522                return updateMessage(accountId, messageId, folderId, flagRead);
523            } else if(table.equals(BluetoothMapContract.TABLE_CONVERSATION)) {
524                return 0; // We do not support changing conversation
525            } else if(table.equals(BluetoothMapContract.TABLE_CONVOCONTACT)) {
526                return 0; // We do not support changing contacts
527            } else {
528                if(D)Log.w(TAG, "Unknown table name: " + table);
529                return 0;
530            }
531        } finally {
532            Binder.restoreCallingIdentity(callingId);
533        }
534    }
535
536    /**
537     * Update an entry in the account table. Only the expose flag will be
538     * changed through this interface.
539     * @param accountId the ID of the account to change.
540     * @param flagExpose the updated value.
541     * @return the number of entries changed - 0 if account not found or value cannot be changed.
542     */
543    abstract protected int updateAccount(String accountId, Integer flagExpose);
544
545    /**
546     * Update an entry in the message table.
547     * @param accountId ID of the account to which the messageId relates
548     * @param messageId the ID of the message to update
549     * @param folderId the new folder ID value to set - ignore if null.
550     * @param flagRead the new flagRead value to set - ignore if null.
551     * @return
552     */
553    abstract protected int updateMessage(String accountId, Long messageId, Long folderId,
554            Boolean flagRead);
555
556    /**
557     * Utility function to Creates a ContentValues object based on a modified valuesSet.
558     * To be used after changing the keys and optionally values of a valueSet obtained
559     * from a ContentValues object received in update().
560     * @param valueSet the values as received in the contentProvider
561     * @param keyMap the key map <btKey, emailKey>
562     * @return a new ContentValues object with the keys replaced as specified in the
563     * keyMap
564     */
565    protected ContentValues createContentValues(Set<Entry<String,Object>> valueSet,
566            Map<String, String> keyMap) {
567        ContentValues values = new ContentValues(valueSet.size());
568        for(Entry<String,Object> ent : valueSet) {
569            String key = keyMap.get(ent.getKey()); // Convert the key name
570            Object value = ent.getValue();
571            if(value == null) {
572                values.putNull(key);
573            } else if(ent.getValue() instanceof Boolean) {
574                values.put(key, (Boolean) value);
575            } else if(ent.getValue() instanceof Byte) {
576                values.put(key, (Byte) value);
577            } else if(ent.getValue() instanceof byte[]) {
578                values.put(key, (byte[]) value);
579            } else if(ent.getValue() instanceof Double) {
580                values.put(key, (Double) value);
581            } else if(ent.getValue() instanceof Float) {
582                values.put(key, (Float) value);
583            } else if(ent.getValue() instanceof Integer) {
584                values.put(key, (Integer) value);
585            } else if(ent.getValue() instanceof Long) {
586                values.put(key, (Long) value);
587            } else if(ent.getValue() instanceof Short) {
588                values.put(key, (Short) value);
589            } else if(ent.getValue() instanceof String) {
590                values.put(key, (String) value);
591            } else {
592                throw new IllegalArgumentException("Unknown data type in content value");
593            }
594        }
595        return values;
596    }
597
598    @Override
599    public Bundle call(String method, String arg, Bundle extras) {
600        long callingId = Binder.clearCallingIdentity();
601        if(D)Log.w(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: "
602                + Thread.currentThread().getId());
603        int ret = -1;
604        try {
605            if(method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) {
606                long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1);
607                if(accountId == -1) {
608                    Log.w(TAG, "No account ID in CALL");
609                    return null;
610                }
611                long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1);
612                if(folderId == -1) {
613                    Log.w(TAG, "No folder ID in CALL");
614                    return null;
615                }
616                ret = syncFolder(accountId, folderId);
617            } else if (method.equals(BluetoothMapContract.METHOD_SET_OWNER_STATUS)) {
618                int presenceState = extras.getInt(BluetoothMapContract.EXTRA_PRESENCE_STATE);
619                String presenceStatus = extras.getString(
620                        BluetoothMapContract.EXTRA_PRESENCE_STATUS);
621                long lastActive = extras.getLong(BluetoothMapContract.EXTRA_LAST_ACTIVE);
622                int chatState = extras.getInt(BluetoothMapContract.EXTRA_CHAT_STATE);
623                String convoId = extras.getString(BluetoothMapContract.EXTRA_CONVERSATION_ID);
624                ret = setOwnerStatus(presenceState, presenceStatus, lastActive, chatState, convoId);
625
626            } else if (method.equals(BluetoothMapContract.METHOD_SET_BLUETOOTH_STATE)) {
627                boolean bluetoothState = extras.getBoolean(
628                        BluetoothMapContract.EXTRA_BLUETOOTH_STATE);
629                ret = setBluetoothStatus(bluetoothState);
630            }
631        } finally {
632            Binder.restoreCallingIdentity(callingId);
633        }
634        if(ret == 0) {
635            return new Bundle();
636        }
637        return null;
638    }
639
640    /**
641     * Trigger a sync of the specified folder.
642     * @param accountId the ID of the account that owns the folder
643     * @param folderId the ID of the folder.
644     * @return 0 at success
645     */
646    abstract protected int syncFolder(long accountId, long folderId);
647
648    /**
649     * Set the properties that should change presence or chat state of owner
650     * e.g. when the owner is active on a BT client device but not on the BT server device
651     * where the IM application is installed, it should still be possible to show an active status.
652     * @param presenceState should follow the contract specified values
653     * @param presenceStatus string the owners current status
654     * @param lastActive time stamp of the owners last activity
655     * @param chatState should follow the contract specified values
656     * @param convoId ID to the conversation to change
657     * @return 0 at success
658     */
659    abstract protected int setOwnerStatus(int presenceState, String presenceStatus,
660            long lastActive, int chatState, String convoId);
661
662    /**
663     * Notify the application of the Bluetooth state
664     * @param bluetoothState 'on' of 'off'
665     * @return 0 at success
666     */
667    abstract protected int setBluetoothStatus(boolean bluetoothState);
668
669
670
671    /**
672     * Need this to suppress warning in unit tests.
673     */
674    @Override
675    public void shutdown() {
676        // Don't call super.shutdown(), which emits a warning...
677    }
678
679    /**
680     * Extract the BluetoothMapContract.AccountColumns._ID from the given URI.
681     */
682    public static String getAccountId(Uri uri) {
683        final List<String> segments = uri.getPathSegments();
684        if (segments.size() < 1) {
685            throw new IllegalArgumentException("No AccountId pressent in URI: " + uri);
686        }
687        return segments.get(0);
688    }
689}
690