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