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