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