• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.mapapi;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.UriMatcher;
24 import android.content.pm.ProviderInfo;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Binder;
29 import android.os.Bundle;
30 import android.os.ParcelFileDescriptor;
31 import android.util.Log;
32 
33 import java.io.FileInputStream;
34 import java.io.FileNotFoundException;
35 import java.io.FileOutputStream;
36 import java.io.IOException;
37 import java.util.List;
38 import java.util.Map;
39 
40 /**
41  * A base implementation of the BluetoothMapEmailContract.
42  * A base class for a ContentProvider that allows access to Email messages from a Bluetooth
43  * device through the Message Access Profile.
44  */
45 public abstract class BluetoothMapEmailProvider extends ContentProvider {
46 
47     private static final String TAG = "BluetoothMapEmailProvider";
48     private static final boolean D = true;
49 
50     private static final int MATCH_ACCOUNT = 1;
51     private static final int MATCH_MESSAGE = 2;
52     private static final int MATCH_FOLDER = 3;
53 
54     protected ContentResolver mResolver;
55 
56     private Uri CONTENT_URI = null;
57     private String mAuthority;
58     private UriMatcher mMatcher;
59 
60 
61     private PipeReader mPipeReader = new PipeReader();
62     private PipeWriter mPipeWriter = new PipeWriter();
63 
64     /**
65      * Write the content of a message to a stream as MIME encoded RFC-2822 data.
66      * @param accountId the ID of the account to which the message belong
67      * @param messageId the ID of the message to write to the stream
68      * @param includeAttachment true if attachments should be included
69      * @param download true if any missing part of the message shall be downloaded
70      *        before written to the stream. The download flag will determine
71      *        whether or not attachments shall be downloaded or only the message content.
72      * @param out the FileOurputStream to write to.
73      * @throws IOException
74      */
WriteMessageToStream(long accountId, long messageId, boolean includeAttachment, boolean download, FileOutputStream out)75     abstract protected void WriteMessageToStream(long accountId, long messageId,
76             boolean includeAttachment, boolean download, FileOutputStream out)
77         throws IOException;
78 
79     /**
80      * @return the CONTENT_URI exposed. This will be used to send out notifications.
81      */
getContentUri()82     abstract protected Uri getContentUri();
83 
84    /**
85     * Implementation is provided by the parent class.
86     */
87    @Override
attachInfo(Context context, ProviderInfo info)88    public void attachInfo(Context context, ProviderInfo info) {
89        mAuthority = info.authority;
90 
91        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
92        mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT);
93        mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_FOLDER, MATCH_FOLDER);
94        mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE);
95 
96        // Sanity check our setup
97        if (!info.exported) {
98            throw new SecurityException("Provider must be exported");
99        }
100        // Enforce correct permissions are used
101        if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)){
102            throw new SecurityException("Provider must be protected by " +
103                    android.Manifest.permission.BLUETOOTH_MAP);
104        }
105        mResolver = context.getContentResolver();
106        super.attachInfo(context, info);
107    }
108 
109 
110     /**
111      * Interface to write a stream of data to a pipe.  Use with
112      * {@link ContentProvider#openPipeHelper}.
113      */
114     public interface PipeDataReader<T> {
115         /**
116          * Called from a background thread to stream data from a pipe.
117          * Note that the pipe is blocking, so this thread can block on
118          * reads for an arbitrary amount of time if the client is slow
119          * at writing.
120          *
121          * @param input The pipe where data should be read. This will be
122          * closed for you upon returning from this function.
123          * @param uri The URI whose data is to be written.
124          * @param mimeType The desired type of data to be written.
125          * @param opts Options supplied by caller.
126          * @param args Your own custom arguments.
127          */
readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, Bundle opts, T args)128         public void readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType,
129                 Bundle opts, T args);
130     }
131 
132     public class PipeReader implements PipeDataReader<Cursor> {
133         /**
134          * Read the data from the pipe and generate a message.
135          * Use the message to do an update of the message specified by the URI.
136          */
137         @Override
readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, Bundle opts, Cursor args)138         public void readDataFromPipe(ParcelFileDescriptor input, Uri uri,
139                 String mimeType, Bundle opts, Cursor args) {
140             Log.v(TAG, "readDataFromPipe(): uri=" + uri.toString());
141             FileInputStream fIn = null;
142             try {
143                 fIn = new FileInputStream(input.getFileDescriptor());
144                 long messageId = Long.valueOf(uri.getLastPathSegment());
145                 long accountId = Long.valueOf(getAccountId(uri));
146                 UpdateMimeMessageFromStream(fIn, accountId, messageId);
147             } catch (IOException e) {
148                 Log.w(TAG,"IOException: ", e);
149                 /* TODO: How to signal the error to the calling entity? Had expected readDataFromPipe
150                  *       to throw IOException?
151                  */
152             } finally {
153                 try {
154                     if(fIn != null)
155                         fIn.close();
156                 } catch (IOException e) {
157                     Log.w(TAG,e);
158                 }
159             }
160         }
161     }
162 
163     /**
164      * Read a MIME encoded RFC-2822 fileStream and update the message content.
165      * The Date and/or From headers may not be present in the MIME encoded
166      * message, and this function shall add appropriate values if the headers
167      * are missing. From should be set to the owner of the account.
168      *
169      * @param input the file stream to read data from
170      * @param accountId the accountId
171      * @param messageId ID of the message to update
172      */
UpdateMimeMessageFromStream(FileInputStream input, long accountId, long messageId)173     abstract protected void UpdateMimeMessageFromStream(FileInputStream input, long accountId,
174             long messageId) throws IOException;
175 
176     public class PipeWriter implements PipeDataWriter<Cursor> {
177         /**
178          * Generate a message based on the cursor, and write the encoded data to the stream.
179          */
180 
writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Cursor c)181         public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
182                 Bundle opts, Cursor c) {
183             if (D) Log.d(TAG, "writeDataToPipe(): uri=" + uri.toString() +
184                     " - getLastPathSegment() = " + uri.getLastPathSegment());
185 
186             FileOutputStream fout = null;
187 
188             try {
189                 fout = new FileOutputStream(output.getFileDescriptor());
190 
191                 boolean includeAttachments = true;
192                 boolean download = false;
193                 List<String> segments = uri.getPathSegments();
194                 long messageId = Long.parseLong(segments.get(2));
195                 long accountId = Long.parseLong(getAccountId(uri));
196                 if(segments.size() >= 4) {
197                     String format = segments.get(3);
198                     if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS)) {
199                         includeAttachments = false;
200                     } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD_NO_ATTACHMENTS)) {
201                         includeAttachments = false;
202                         download = true;
203                     } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD)) {
204                         download = true;
205                     }
206                 }
207 
208                 WriteMessageToStream(accountId, messageId, includeAttachments, download, fout);
209             } catch (IOException e) {
210                 Log.w(TAG, e);
211                 /* TODO: How to signal the error to the calling entity? Had expected writeDataToPipe
212                  *       to throw IOException?
213                  */
214             } finally {
215                 try {
216                     fout.flush();
217                 } catch (IOException e) {
218                     Log.w(TAG, "IOException: ", e);
219                 }
220                 try {
221                     fout.close();
222                 } catch (IOException e) {
223                     Log.w(TAG, "IOException: ", e);
224                 }
225             }
226         }
227     }
228 
229     /**
230      * This function shall be called when any Account database content have changed
231      * to Notify any attached observers.
232      * @param accountId the ID of the account that changed. Null is a valid value,
233      *        if accountId is unknown or multiple accounts changed.
234      */
onAccountChanged(String accountId)235     protected void onAccountChanged(String accountId) {
236         Uri newUri = null;
237 
238         if(mAuthority == null){
239             return;
240         }
241         if(accountId == null){
242             newUri = BluetoothMapContract.buildAccountUri(mAuthority);
243         } else {
244             newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId);
245         }
246         if(D) Log.d(TAG,"onAccountChanged() accountId = " + accountId + " URI: " + newUri);
247         mResolver.notifyChange(newUri, null);
248     }
249 
250     /**
251      * This function shall be called when any Message database content have changed
252      * to notify any attached observers.
253      * @param accountId Null is a valid value, if accountId is unknown, but
254      *        recommended for increased performance.
255      * @param messageId Null is a valid value, if multiple messages changed or the
256      *        messageId is unknown, but recommended for increased performance.
257      */
onMessageChanged(String accountId, String messageId)258     protected void onMessageChanged(String accountId, String messageId) {
259         Uri newUri = null;
260 
261         if(mAuthority == null){
262             return;
263         }
264 
265         if(accountId == null){
266             newUri = BluetoothMapContract.buildMessageUri(mAuthority);
267         } else {
268             if(messageId == null)
269             {
270                 newUri = BluetoothMapContract.buildMessageUri(mAuthority,accountId);
271             } else {
272                 newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority,accountId, messageId);
273             }
274         }
275         if(D) Log.d(TAG,"onMessageChanged() accountId = " + accountId + " messageId = " + messageId + " URI: " + newUri);
276         mResolver.notifyChange(newUri, null);
277     }
278 
279     /**
280      * Not used, this is just a dummy implementation.
281      */
282     @Override
getType(Uri uri)283     public String getType(Uri uri) {
284         return "Email";
285     }
286 
287     /**
288      * Open a file descriptor to a message.
289      * Two modes supported for read: With and without attachments.
290      * One mode exist for write and the actual content will be with or without
291      * attachments.
292      *
293      * Mode will be "r" or "w".
294      *
295      * URI format:
296      * The URI scheme is as follows.
297      * For messages with attachments:
298      *   content://com.android.mail.bluetoothprovider/Messages/msgId#
299      *
300      * For messages without attachments:
301      *   content://com.android.mail.bluetoothprovider/Messages/msgId#/NO_ATTACHMENTS
302      *
303      * UPDATE: For write.
304      *         First create a message in the DB using insert into the message DB
305      *         Then open a file handle to the #id
306      *         write the data to a stream created from the fileHandle.
307      *
308      * @param uri the URI to open. ../Messages/#id
309      * @param mode the mode to use. The following modes exist: - UPDATE do not work - use URI
310      *  - "read_with_attachments" - to read an e-mail including any attachments
311      *  - "read_no_attachments" - to read an e-mail excluding any attachments
312      *  - "write" - to add a mime encoded message to the database. This write
313      *              should not trigger the message to be send.
314      * @return the ParcelFileDescriptor
315      *  @throws FileNotFoundException
316      */
317     @Override
openFile(Uri uri, String mode)318     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
319         long callingId = Binder.clearCallingIdentity();
320         if(D)Log.d(TAG, "openFile(): uri=" + uri.toString() + " - getLastPathSegment() = " +
321                 uri.getLastPathSegment());
322         try {
323             /* To be able to do abstraction of the file IO, we simply ignore the URI at this
324              * point and let the read/write function implementations parse the URI. */
325             if(mode.equals("w")) {
326                 return openInversePipeHelper(uri, null, null, null, mPipeReader);
327             } else {
328                 return openPipeHelper (uri, null, null, null, mPipeWriter);
329             }
330         } catch (IOException e) {
331             Log.w(TAG,e);
332         } finally {
333             Binder.restoreCallingIdentity(callingId);
334         }
335         return null;
336     }
337 
338     /**
339      * A helper function for implementing {@link #openFile}, for
340      * creating a data pipe and background thread allowing you to stream
341      * data back from the client.  This function returns a new
342      * ParcelFileDescriptor that should be returned to the caller (the caller
343      * is responsible for closing it).
344      *
345      * @param uri The URI whose data is to be written.
346      * @param mimeType The desired type of data to be written.
347      * @param opts Options supplied by caller.
348      * @param args Your own custom arguments.
349      * @param func Interface implementing the function that will actually
350      * stream the data.
351      * @return Returns a new ParcelFileDescriptor holding the read side of
352      * the pipe.  This should be returned to the caller for reading; the caller
353      * is responsible for closing it when done.
354      */
openInversePipeHelper(final Uri uri, final String mimeType, final Bundle opts, final T args, final PipeDataReader<T> func)355     private <T> ParcelFileDescriptor openInversePipeHelper(final Uri uri, final String mimeType,
356             final Bundle opts, final T args, final PipeDataReader<T> func)
357             throws FileNotFoundException {
358         try {
359             final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
360 
361             AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
362                 @Override
363                 protected Object doInBackground(Object... params) {
364                     func.readDataFromPipe(fds[0], uri, mimeType, opts, args);
365                     try {
366                         fds[0].close();
367                     } catch (IOException e) {
368                         Log.w(TAG, "Failure closing pipe", e);
369                     }
370                     return null;
371                 }
372             };
373             task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])null);
374 
375             return fds[1];
376         } catch (IOException e) {
377             throw new FileNotFoundException("failure making pipe");
378         }
379     }
380 
381     /**
382      * The MAP specification states that a delete request from MAP client is a folder shift to the
383      * 'deleted' folder.
384      * Only use case of delete() is when transparency is requested for push messages, then
385      * message should not remain in sent folder and therefore must be deleted
386      */
387     @Override
delete(Uri uri, String where, String[] selectionArgs)388     public int delete(Uri uri, String where, String[] selectionArgs) {
389         if (D) Log.d(TAG, "delete(): uri=" + uri.toString() );
390         int result = 0;
391 
392         String table = uri.getPathSegments().get(1);
393         if(table == null)
394             throw new IllegalArgumentException("Table missing in URI");
395         // the id of the entry to be deleted from the database
396         String messageId = uri.getLastPathSegment();
397         if (messageId == null)
398             throw new IllegalArgumentException("Message ID missing in update values!");
399 
400 
401         String accountId = getAccountId(uri);
402         if (accountId == null)
403             throw new IllegalArgumentException("Account ID missing in update values!");
404 
405         long callingId = Binder.clearCallingIdentity();
406         try {
407             if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
408                 return deleteMessage(accountId, messageId);
409             } else {
410                 if (D) Log.w(TAG, "Unknown table name: " + table);
411                 return result;
412             }
413         } finally {
414             Binder.restoreCallingIdentity(callingId);
415         }
416     }
417 
418     /**
419      * This function deletes a message.
420      * @param accountId the ID of the Account
421      * @param messageId the ID of the message to delete.
422      * @return the number of messages deleted - 0 if the message was not found.
423      */
deleteMessage(String accountId, String messageId)424     abstract protected int deleteMessage(String accountId, String messageId);
425 
426     /**
427      * Insert is used to add new messages to the data base.
428      * Insert message approach:
429      *   - Insert an empty message to get an _id with only a folder_id
430      *   - Open the _id for write
431      *   - Write the message content
432      *     (When the writer completes, this provider should do an update of the message)
433      */
434     @Override
insert(Uri uri, ContentValues values)435     public Uri insert(Uri uri, ContentValues values) {
436         String table = uri.getLastPathSegment();
437         if(table == null){
438             throw new IllegalArgumentException("Table missing in URI");
439         }
440         String accountId = getAccountId(uri);
441         Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID);
442         if(folderId == null) {
443             throw new IllegalArgumentException("FolderId missing in ContentValues");
444         }
445 
446         String id; // the id of the entry inserted into the database
447         long callingId = Binder.clearCallingIdentity();
448         Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " +
449                 uri.getLastPathSegment());
450         try {
451             if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
452                 id = insertMessage(accountId, folderId.toString());
453                 if(D) Log.i(TAG, "insert() ID: " + id);
454                 return Uri.parse(uri.toString() + "/" + id);
455             } else {
456                 Log.w(TAG, "Unknown table name: " + table);
457                 return null;
458             }
459         } finally {
460             Binder.restoreCallingIdentity(callingId);
461         }
462     }
463 
464 
465     /**
466      * Inserts an empty message into the Message data base in the specified folder.
467      * This is done before the actual message content is written by fileIO.
468      * @param accountId the ID of the account
469      * @param folderId the ID of the folder to create a new message in.
470      * @return the message id as a string
471      */
insertMessage(String accountId, String folderId)472     abstract protected String insertMessage(String accountId, String folderId);
473 
474      /**
475      * Utility function to build a projection based on a projectionMap.
476      *
477      *   "btColumnName" -> "emailColumnName as btColumnName" for each entry.
478      *
479      * This supports SQL statements in the emailColumnName entry.
480      * @param projection
481      * @param projectionMap <btColumnName, emailColumnName>
482      * @return the converted projection
483      */
convertProjection(String[] projection, Map<String,String> projectionMap)484     protected String[] convertProjection(String[] projection, Map<String,String> projectionMap) {
485         String[] newProjection = new String[projection.length];
486         for(int i = 0; i < projection.length; i++) {
487             newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i];
488         }
489         return newProjection;
490     }
491 
492     /**
493      * This query needs to map from the data used in the e-mail client to BluetoothMapContract type of data.
494      */
495     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)496     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
497             String sortOrder) {
498         long callingId = Binder.clearCallingIdentity();
499         try {
500             String accountId = null;
501             switch (mMatcher.match(uri)) {
502                 case MATCH_ACCOUNT:
503                     return queryAccount(projection, selection, selectionArgs, sortOrder);
504                 case MATCH_FOLDER:
505                     accountId = getAccountId(uri);
506                     return queryFolder(accountId, projection, selection, selectionArgs, sortOrder);
507                 case MATCH_MESSAGE:
508                     accountId = getAccountId(uri);
509                     return queryMessage(accountId, projection, selection, selectionArgs, sortOrder);
510                 default:
511                     throw new UnsupportedOperationException("Unsupported Uri " + uri);
512             }
513         } finally {
514             Binder.restoreCallingIdentity(callingId);
515         }
516     }
517 
518     /**
519      * Query account information.
520      * This function shall return only exposable e-mail accounts. Hence shall not
521      * return accounts that has policies suggesting not to be shared them.
522      * @param projection
523      * @param selection
524      * @param selectionArgs
525      * @param sortOrder
526      * @return a cursor to the accounts that are subject to exposure over BT.
527      */
queryAccount(String[] projection, String selection, String[] selectionArgs, String sortOrder)528     abstract protected Cursor queryAccount(String[] projection, String selection, String[] selectionArgs,
529             String sortOrder);
530 
531     /**
532      * Filter out the non usable folders and ensure to name the mandatory folders
533      * inbox, outbox, sent, deleted and draft.
534      * @param accountId
535      * @param projection
536      * @param selection
537      * @param selectionArgs
538      * @param sortOrder
539      * @return
540      */
queryFolder(String accountId, String[] projection, String selection, String[] selectionArgs, String sortOrder)541     abstract protected Cursor queryFolder(String accountId, String[] projection, String selection, String[] selectionArgs,
542             String sortOrder);
543     /**
544      * For the message table the selection (where clause) can only include the following columns:
545      *    date: less than, greater than and equals
546      *    flagRead: = 1 or = 0
547      *    flagPriority: = 1 or = 0
548      *    folder_id: the ID of the folder only equals
549      *    toList: partial name/address search
550      *    ccList: partial name/address search
551      *    bccList: partial name/address search
552      *    fromList: partial name/address search
553      * Additionally the COUNT and OFFSET shall be supported.
554      * @param accountId the ID of the account
555      * @param projection
556      * @param selection
557      * @param selectionArgs
558      * @param sortOrder
559      * @return a cursor to query result
560      */
queryMessage(String accountId, String[] projection, String selection, String[] selectionArgs, String sortOrder)561     abstract protected Cursor queryMessage(String accountId, String[] projection, String selection, String[] selectionArgs,
562             String sortOrder);
563 
564     /**
565      * update()
566      * Messages can be modified in the following cases:
567      *  - the folder_key of a message - hence the message can be moved to a new folder,
568      *                                  but the content cannot be modified.
569      *  - the FLAG_READ state can be changed.
570      * The selection statement will always be selection of a message ID, when updating a message,
571      * hence this function will be called multiple times if multiple messages must be updated
572      * due to the nature of the Bluetooth Message Access profile.
573      */
574     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)575     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
576 
577         String table = uri.getLastPathSegment();
578         if(table == null){
579             throw new IllegalArgumentException("Table missing in URI");
580         }
581         if(selection != null) {
582             throw new IllegalArgumentException("selection shall not be used, ContentValues shall contain the data");
583         }
584 
585         long callingId = Binder.clearCallingIdentity();
586         if(D)Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " +
587                 uri.getLastPathSegment());
588         try {
589             if(table.equals(BluetoothMapContract.TABLE_ACCOUNT)) {
590                 String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID);
591                 if(accountId == null) {
592                     throw new IllegalArgumentException("Account ID missing in update values!");
593                 }
594                 Integer exposeFlag = values.getAsInteger(BluetoothMapContract.AccountColumns.FLAG_EXPOSE);
595                 if(exposeFlag == null){
596                     throw new IllegalArgumentException("Expose flag missing in update values!");
597                 }
598                 return updateAccount(accountId, exposeFlag);
599             } else if(table.equals(BluetoothMapContract.TABLE_FOLDER)) {
600                 return 0; // We do not support changing folders
601             } else if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
602                 String accountId = getAccountId(uri);
603                 Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID);
604                 if(messageId == null) {
605                     throw new IllegalArgumentException("Message ID missing in update values!");
606                 }
607                 Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID);
608                 Boolean flagRead = values.getAsBoolean(BluetoothMapContract.MessageColumns.FLAG_READ);
609                 return updateMessage(accountId, messageId, folderId, flagRead);
610             } else {
611                 if(D)Log.w(TAG, "Unknown table name: " + table);
612                 return 0;
613             }
614         } finally {
615             Binder.restoreCallingIdentity(callingId);
616         }
617     }
618 
619     /**
620      * Update an entry in the account table. Only the expose flag will be
621      * changed through this interface.
622      * @param accountId the ID of the account to change.
623      * @param flagExpose the updated value.
624      * @return the number of entries changed - 0 if account not found or value cannot be changed.
625      */
updateAccount(String accountId, int flagExpose)626     abstract protected int updateAccount(String accountId, int flagExpose);
627 
628     /**
629      * Update an entry in the message table.
630      * @param accountId ID of the account to which the messageId relates
631      * @param messageId the ID of the message to update
632      * @param folderId the new folder ID value to set - ignore if null.
633      * @param flagRead the new flagRead value to set - ignore if null.
634      * @return
635      */
updateMessage(String accountId, Long messageId, Long folderId, Boolean flagRead)636     abstract protected int updateMessage(String accountId, Long messageId, Long folderId, Boolean flagRead);
637 
638 
639     @Override
call(String method, String arg, Bundle extras)640     public Bundle call(String method, String arg, Bundle extras) {
641         long callingId = Binder.clearCallingIdentity();
642         if(D)Log.d(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " + Thread.currentThread().getId());
643 
644         try {
645             if(method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) {
646                 long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1);
647                 if(accountId == -1) {
648                     Log.w(TAG, "No account ID in CALL");
649                     return null;
650                 }
651                 long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1);
652                 if(folderId == -1) {
653                     Log.w(TAG, "No folder ID in CALL");
654                     return null;
655                 }
656                 int ret = syncFolder(accountId, folderId);
657                 if(ret == 0) {
658                     return new Bundle();
659                 }
660                 return null;
661             }
662         } finally {
663             Binder.restoreCallingIdentity(callingId);
664         }
665         return null;
666     }
667 
668     /**
669      * Trigger a sync of the specified folder.
670      * @param accountId the ID of the account that owns the folder
671      * @param folderId the ID of the folder.
672      * @return 0 at success
673      */
syncFolder(long accountId, long folderId)674     abstract protected int syncFolder(long accountId, long folderId);
675 
676     /**
677      * Need this to suppress warning in unit tests.
678      */
679     @Override
shutdown()680     public void shutdown() {
681         // Don't call super.shutdown(), which emits a warning...
682     }
683 
684     /**
685      * Extract the BluetoothMapContract.AccountColumns._ID from the given URI.
686      */
getAccountId(Uri uri)687     public static String getAccountId(Uri uri) {
688         final List<String> segments = uri.getPathSegments();
689         if (segments.size() < 1) {
690             throw new IllegalArgumentException("No AccountId pressent in URI: " + uri);
691         }
692         return segments.get(0);
693     }
694 }
695