1 /*
2  * Copyright (c) 2008-2009, Motorola, Inc.
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * - Redistributions of source code must retain the above copyright notice,
10  * this list of conditions and the following disclaimer.
11  *
12  * - Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution.
15  *
16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
17  * may be used to endorse or promote products derived from this software
18  * without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.android.bluetooth.opp;
34 
35 import android.content.ContentValues;
36 import android.content.Context;
37 import android.net.Uri;
38 import android.os.Handler;
39 import android.os.Message;
40 import android.os.PowerManager;
41 import android.os.PowerManager.WakeLock;
42 import android.os.Process;
43 import android.os.SystemClock;
44 import android.util.Log;
45 
46 import com.android.bluetooth.BluetoothMetricsProto;
47 import com.android.bluetooth.btservice.MetricsLogger;
48 
49 import java.io.BufferedInputStream;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.OutputStream;
53 
54 import javax.obex.ClientOperation;
55 import javax.obex.ClientSession;
56 import javax.obex.HeaderSet;
57 import javax.obex.ObexTransport;
58 import javax.obex.ResponseCodes;
59 
60 /**
61  * This class runs as an OBEX client
62  */
63 public class BluetoothOppObexClientSession implements BluetoothOppObexSession {
64 
65     private static final String TAG = "BtOppObexClient";
66     private static final boolean D = Constants.DEBUG;
67     private static final boolean V = Constants.VERBOSE;
68 
69     private ClientThread mThread;
70 
71     private ObexTransport mTransport;
72 
73     private Context mContext;
74 
75     private volatile boolean mInterrupted;
76 
77     private volatile boolean mWaitingForRemote;
78 
79     private Handler mCallback;
80 
81     private int mNumFilesAttemptedToSend;
82 
BluetoothOppObexClientSession(Context context, ObexTransport transport)83     public BluetoothOppObexClientSession(Context context, ObexTransport transport) {
84         if (transport == null) {
85             throw new NullPointerException("transport is null");
86         }
87         mContext = context;
88         mTransport = transport;
89     }
90 
91     @Override
start(Handler handler, int numShares)92     public void start(Handler handler, int numShares) {
93         if (D) {
94             Log.d(TAG, "Start!");
95         }
96         mCallback = handler;
97         mThread = new ClientThread(mContext, mTransport, numShares);
98         mThread.start();
99     }
100 
101     @Override
stop()102     public void stop() {
103         if (D) {
104             Log.d(TAG, "Stop!");
105         }
106         if (mThread != null) {
107             mInterrupted = true;
108             try {
109                 mThread.interrupt();
110                 if (V) {
111                     Log.v(TAG, "waiting for thread to terminate");
112                 }
113                 mThread.join();
114                 mThread = null;
115             } catch (InterruptedException e) {
116                 if (V) {
117                     Log.v(TAG, "Interrupted waiting for thread to join");
118                 }
119             }
120         }
121         BluetoothOppUtility.cancelNotification(mContext);
122         mCallback = null;
123     }
124 
125     @Override
addShare(BluetoothOppShareInfo share)126     public void addShare(BluetoothOppShareInfo share) {
127         mThread.addShare(share);
128     }
129 
readFully(InputStream is, byte[] buffer, int size)130     private static int readFully(InputStream is, byte[] buffer, int size) throws IOException {
131         int done = 0;
132         while (done < size) {
133             int got = is.read(buffer, done, size - done);
134             if (got <= 0) {
135                 break;
136             }
137             done += got;
138         }
139         return done;
140     }
141 
142     private class ClientThread extends Thread {
143 
144         private static final int SLEEP_TIME = 500;
145 
146         private Context mContext1;
147 
148         private BluetoothOppShareInfo mInfo;
149 
150         private volatile boolean mWaitingForShare;
151 
152         private ObexTransport mTransport1;
153 
154         private ClientSession mCs;
155 
156         private WakeLock mWakeLock;
157 
158         private BluetoothOppSendFileInfo mFileInfo = null;
159 
160         private boolean mConnected = false;
161 
162         private int mNumShares;
163 
ClientThread(Context context, ObexTransport transport, int initialNumShares)164         ClientThread(Context context, ObexTransport transport, int initialNumShares) {
165             super("BtOpp ClientThread");
166             mContext1 = context;
167             mTransport1 = transport;
168             mWaitingForShare = true;
169             mWaitingForRemote = false;
170             mNumShares = initialNumShares;
171             PowerManager pm = (PowerManager) mContext1.getSystemService(Context.POWER_SERVICE);
172             mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
173         }
174 
addShare(BluetoothOppShareInfo info)175         public void addShare(BluetoothOppShareInfo info) {
176             mInfo = info;
177             mFileInfo = processShareInfo();
178             mWaitingForShare = false;
179         }
180 
181         @Override
run()182         public void run() {
183             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
184 
185             if (V) {
186                 Log.v(TAG, "acquire partial WakeLock");
187             }
188             mWakeLock.acquire();
189 
190             try {
191                 Thread.sleep(100);
192             } catch (InterruptedException e1) {
193                 if (V) {
194                     Log.v(TAG, "Client thread was interrupted (1), exiting");
195                 }
196                 mInterrupted = true;
197             }
198             if (!mInterrupted) {
199                 connect(mNumShares);
200             }
201 
202             mNumFilesAttemptedToSend = 0;
203             while (!mInterrupted) {
204                 if (!mWaitingForShare) {
205                     doSend();
206                 } else {
207                     try {
208                         if (D) {
209                             Log.d(TAG, "Client thread waiting for next share, sleep for "
210                                     + SLEEP_TIME);
211                         }
212                         Thread.sleep(SLEEP_TIME);
213                     } catch (InterruptedException e) {
214 
215                     }
216                 }
217             }
218             disconnect();
219 
220             if (mWakeLock.isHeld()) {
221                 if (V) {
222                     Log.v(TAG, "release partial WakeLock");
223                 }
224                 mWakeLock.release();
225             }
226 
227             if (mNumFilesAttemptedToSend > 0) {
228                 // Log outgoing OPP transfer if more than one file is accepted by remote
229                 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.OPP);
230             }
231             Message msg = Message.obtain(mCallback);
232             msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
233             msg.obj = mInfo;
234             msg.sendToTarget();
235 
236         }
237 
disconnect()238         private void disconnect() {
239             try {
240                 if (mCs != null) {
241                     mCs.disconnect(null);
242                 }
243                 mCs = null;
244                 if (D) {
245                     Log.d(TAG, "OBEX session disconnected");
246                 }
247             } catch (IOException e) {
248                 Log.w(TAG, "OBEX session disconnect error" + e);
249             }
250             try {
251                 if (mCs != null) {
252                     if (D) {
253                         Log.d(TAG, "OBEX session close mCs");
254                     }
255                     mCs.close();
256                     if (D) {
257                         Log.d(TAG, "OBEX session closed");
258                     }
259                 }
260             } catch (IOException e) {
261                 Log.w(TAG, "OBEX session close error" + e);
262             }
263             if (mTransport1 != null) {
264                 try {
265                     mTransport1.close();
266                 } catch (IOException e) {
267                     Log.e(TAG, "mTransport.close error");
268                 }
269 
270             }
271         }
272 
connect(int numShares)273         private void connect(int numShares) {
274             if (D) {
275                 Log.d(TAG, "Create ClientSession with transport " + mTransport1.toString());
276             }
277             try {
278                 mCs = new ClientSession(mTransport1);
279                 mConnected = true;
280             } catch (IOException e1) {
281                 Log.e(TAG, "OBEX session create error");
282             }
283             if (mConnected) {
284                 mConnected = false;
285                 HeaderSet hs = new HeaderSet();
286                 hs.setHeader(HeaderSet.COUNT, (long) numShares);
287                 synchronized (this) {
288                     mWaitingForRemote = true;
289                 }
290                 try {
291                     mCs.connect(hs);
292                     if (D) {
293                         Log.d(TAG, "OBEX session created");
294                     }
295                     mConnected = true;
296                 } catch (IOException e) {
297                     Log.e(TAG, "OBEX session connect error");
298                 }
299             }
300             synchronized (this) {
301                 mWaitingForRemote = false;
302             }
303         }
304 
doSend()305         private void doSend() {
306 
307             int status = BluetoothShare.STATUS_SUCCESS;
308 
309             /* connection is established too fast to get first mInfo */
310             while (mFileInfo == null) {
311                 try {
312                     Thread.sleep(50);
313                 } catch (InterruptedException e) {
314                     status = BluetoothShare.STATUS_CANCELED;
315                 }
316             }
317             if (!mConnected) {
318                 // Obex connection error
319                 status = BluetoothShare.STATUS_CONNECTION_ERROR;
320             }
321             if (status == BluetoothShare.STATUS_SUCCESS) {
322                 /* do real send */
323                 if (mFileInfo.mFileName != null) {
324                     status = sendFile(mFileInfo);
325                 } else {
326                     /* this is invalid request */
327                     status = mFileInfo.mStatus;
328                 }
329                 mWaitingForShare = true;
330             } else {
331                 Constants.updateShareStatus(mContext1, mInfo.mId, status);
332             }
333 
334             Message msg = Message.obtain(mCallback);
335             msg.what = (status == BluetoothShare.STATUS_SUCCESS)
336                     ? BluetoothOppObexSession.MSG_SHARE_COMPLETE
337                     : BluetoothOppObexSession.MSG_SESSION_ERROR;
338             mInfo.mStatus = status;
339             msg.obj = mInfo;
340             msg.sendToTarget();
341         }
342 
343         /*
344          * Validate this ShareInfo
345          */
processShareInfo()346         private BluetoothOppSendFileInfo processShareInfo() {
347             if (V) {
348                 Log.v(TAG, "Client thread processShareInfo() " + mInfo.mId);
349             }
350 
351             BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(mInfo.mUri);
352             if (fileInfo.mFileName == null || fileInfo.mLength == 0) {
353                 if (V) {
354                     Log.v(TAG, "BluetoothOppSendFileInfo get invalid file");
355                 }
356                 Constants.updateShareStatus(mContext1, mInfo.mId, fileInfo.mStatus);
357 
358             } else {
359                 if (V) {
360                     Log.v(TAG, "Generate BluetoothOppSendFileInfo:");
361                     Log.v(TAG, "filename  :" + fileInfo.mFileName);
362                     Log.v(TAG, "length    :" + fileInfo.mLength);
363                     Log.v(TAG, "mimetype  :" + fileInfo.mMimetype);
364                 }
365 
366                 ContentValues updateValues = new ContentValues();
367                 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
368 
369                 updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
370                 updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
371                 updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype);
372 
373                 mContext1.getContentResolver().update(contentUri, updateValues, null, null);
374 
375             }
376             return fileInfo;
377         }
378 
sendFile(BluetoothOppSendFileInfo fileInfo)379         private int sendFile(BluetoothOppSendFileInfo fileInfo) {
380             boolean error = false;
381             int responseCode = -1;
382             long position = 0;
383             int status = BluetoothShare.STATUS_SUCCESS;
384             Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
385             ContentValues updateValues;
386             HeaderSet request = new HeaderSet();
387             ClientOperation putOperation = null;
388             OutputStream outputStream = null;
389             InputStream inputStream = null;
390             try {
391                 synchronized (this) {
392                     mWaitingForRemote = true;
393                 }
394                 try {
395                     if (V) {
396                         Log.v(TAG, "Set header items for " + fileInfo.mFileName);
397                     }
398                     request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
399                     request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);
400 
401                     applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName);
402                     Constants.updateShareStatus(mContext1, mInfo.mId,
403                             BluetoothShare.STATUS_RUNNING);
404 
405                     request.setHeader(HeaderSet.LENGTH, fileInfo.mLength);
406 
407                     if (V) {
408                         Log.v(TAG, "put headerset for " + fileInfo.mFileName);
409                     }
410                     putOperation = (ClientOperation) mCs.put(request);
411                 } catch (IllegalArgumentException e) {
412                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
413                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
414 
415                     Log.e(TAG, "Error setting header items for request: " + e);
416                     error = true;
417                 } catch (IOException e) {
418                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
419                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
420 
421                     Log.e(TAG, "Error when put HeaderSet ");
422                     error = true;
423                 }
424                 synchronized (this) {
425                     mWaitingForRemote = false;
426                 }
427 
428                 if (!error) {
429                     try {
430                         if (V) {
431                             Log.v(TAG, "openOutputStream " + fileInfo.mFileName);
432                         }
433                         outputStream = putOperation.openOutputStream();
434                         inputStream = putOperation.openInputStream();
435                     } catch (IOException e) {
436                         status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
437                         Constants.updateShareStatus(mContext1, mInfo.mId, status);
438                         Log.e(TAG, "Error when openOutputStream");
439                         error = true;
440                     }
441                 }
442                 if (!error) {
443                     updateValues = new ContentValues();
444                     updateValues.put(BluetoothShare.CURRENT_BYTES, 0);
445                     updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
446                     mContext1.getContentResolver().update(contentUri, updateValues, null, null);
447                 }
448 
449                 if (!error) {
450                     int readLength = 0;
451                     long percent = 0;
452                     long prevPercent = 0;
453                     boolean okToProceed = false;
454                     long timestamp = 0;
455                     long currentTime = 0;
456                     long prevTimestamp = SystemClock.elapsedRealtime();
457                     int outputBufferSize = putOperation.getMaxPacketSize();
458                     byte[] buffer = new byte[outputBufferSize];
459                     BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);
460 
461                     if (!mInterrupted && (position != fileInfo.mLength)) {
462                         readLength = readFully(a, buffer, outputBufferSize);
463 
464                         mCallback.sendMessageDelayed(mCallback.obtainMessage(
465                                 BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
466                                 BluetoothOppObexSession.SESSION_TIMEOUT);
467                         synchronized (this) {
468                             mWaitingForRemote = true;
469                         }
470 
471                         // first packet will block here
472                         outputStream.write(buffer, 0, readLength);
473 
474                         position += readLength;
475 
476                         if (position == fileInfo.mLength) {
477                             // if file length is smaller than buffer size, only one packet
478                             // so block point is here
479                             outputStream.close();
480                             outputStream = null;
481                         }
482 
483                         /* check remote accept or reject */
484                         responseCode = putOperation.getResponseCode();
485 
486                         mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
487                         synchronized (this) {
488                             mWaitingForRemote = false;
489                         }
490 
491                         if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
492                                 || responseCode == ResponseCodes.OBEX_HTTP_OK) {
493                             if (V) {
494                                 Log.v(TAG, "Remote accept");
495                             }
496                             okToProceed = true;
497                             updateValues = new ContentValues();
498                             updateValues.put(BluetoothShare.CURRENT_BYTES, position);
499                             mContext1.getContentResolver()
500                                     .update(contentUri, updateValues, null, null);
501                             mNumFilesAttemptedToSend++;
502                         } else {
503                             Log.i(TAG, "Remote reject, Response code is " + responseCode);
504                         }
505                     }
506 
507                     while (!mInterrupted && okToProceed && (position < fileInfo.mLength)) {
508                         if (V) {
509                             timestamp = SystemClock.elapsedRealtime();
510                         }
511 
512                         readLength = a.read(buffer, 0, outputBufferSize);
513                         outputStream.write(buffer, 0, readLength);
514 
515                         /* check remote abort */
516                         responseCode = putOperation.getResponseCode();
517                         if (V) {
518                             Log.v(TAG, "Response code is " + responseCode);
519                         }
520                         if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
521                                 && responseCode != ResponseCodes.OBEX_HTTP_OK) {
522                             /* abort happens */
523                             okToProceed = false;
524                         } else {
525                             position += readLength;
526                             currentTime = SystemClock.elapsedRealtime();
527                             if (V) {
528                                 Log.v(TAG, "Sending file position = " + position
529                                         + " readLength " + readLength + " bytes took "
530                                         + (currentTime - timestamp) + " ms");
531                             }
532                             // Update the Progress Bar only if there is change in percentage
533                             // or once per a period to notify NFC of this transfer is still alive
534                             percent = position * 100 / fileInfo.mLength;
535                             if (percent > prevPercent
536                                     || currentTime - prevTimestamp > Constants.NFC_ALIVE_CHECK_MS) {
537                                 updateValues = new ContentValues();
538                                 updateValues.put(BluetoothShare.CURRENT_BYTES, position);
539                                 mContext1.getContentResolver()
540                                         .update(contentUri, updateValues, null, null);
541                                 prevPercent = percent;
542                                 prevTimestamp = currentTime;
543                             }
544                         }
545                     }
546 
547                     if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
548                             || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
549                         Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
550                                 + fileInfo.mLength);
551                         status = BluetoothShare.STATUS_FORBIDDEN;
552                     } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
553                         Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
554                         status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
555                     } else if (!mInterrupted && position == fileInfo.mLength) {
556                         Log.i(TAG,
557                                 "SendFile finished send out file " + fileInfo.mFileName + " length "
558                                         + fileInfo.mLength);
559                     } else {
560                         error = true;
561                         status = BluetoothShare.STATUS_CANCELED;
562                         putOperation.abort();
563                         /* interrupted */
564                         Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
565                                 + " at " + position + " of " + fileInfo.mLength);
566                     }
567                 }
568             } catch (IOException e) {
569                 handleSendException(e.toString());
570             } catch (NullPointerException e) {
571                 handleSendException(e.toString());
572             } catch (IndexOutOfBoundsException e) {
573                 handleSendException(e.toString());
574             } finally {
575                 try {
576                     if (outputStream != null) {
577                         outputStream.close();
578                     }
579                 } catch (IOException e) {
580                     Log.e(TAG, "Error when closing output stream after send");
581                 }
582 
583                 // Close InputStream and remove SendFileInfo from map
584                 BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
585                 try {
586                     if (!error) {
587                         responseCode = putOperation.getResponseCode();
588                         if (responseCode != -1) {
589                             if (V) {
590                                 Log.v(TAG, "Get response code " + responseCode);
591                             }
592                             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
593                                 Log.i(TAG, "Response error code is " + responseCode);
594                                 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
595                                 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
596                                     status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
597                                 }
598                                 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
599                                         || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
600                                     status = BluetoothShare.STATUS_FORBIDDEN;
601                                 }
602                             }
603                         } else {
604                             // responseCode is -1, which means connection error
605                             status = BluetoothShare.STATUS_CONNECTION_ERROR;
606                         }
607                     }
608 
609                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
610 
611                     if (inputStream != null) {
612                         inputStream.close();
613                     }
614                     if (putOperation != null) {
615                         putOperation.close();
616                     }
617                 } catch (IOException e) {
618                     Log.e(TAG, "Error when closing stream after send");
619 
620                     // Socket has been closed due to the response timeout in the framework,
621                     // mark the transfer as failure.
622                     if (position != fileInfo.mLength) {
623                         status = BluetoothShare.STATUS_FORBIDDEN;
624                         Constants.updateShareStatus(mContext1, mInfo.mId, status);
625                     }
626                 }
627             }
628             BluetoothOppUtility.cancelNotification(mContext);
629             return status;
630         }
631 
handleSendException(String exception)632         private void handleSendException(String exception) {
633             Log.e(TAG, "Error when sending file: " + exception);
634             // Update interrupted outbound content resolver entry when
635             // error during transfer.
636             Constants.updateShareStatus(mContext1, mInfo.mId,
637                     BluetoothShare.STATUS_OBEX_DATA_ERROR);
638             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
639         }
640 
641         @Override
interrupt()642         public void interrupt() {
643             super.interrupt();
644             synchronized (this) {
645                 if (mWaitingForRemote) {
646                     if (V) {
647                         Log.v(TAG, "Interrupted when waitingForRemote");
648                     }
649                     try {
650                         mTransport1.close();
651                     } catch (IOException e) {
652                         Log.e(TAG, "mTransport.close error");
653                     }
654                     Message msg = Message.obtain(mCallback);
655                     msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
656                     if (mInfo != null) {
657                         msg.obj = mInfo;
658                     }
659                     msg.sendToTarget();
660                 }
661             }
662         }
663     }
664 
applyRemoteDeviceQuirks(HeaderSet request, String address, String filename)665     public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
666         if (address == null) {
667             return;
668         }
669         if (address.startsWith("00:04:48")) {
670             // Poloroid Pogo
671             // Rejects filenames with more than one '.'. Rename to '_'.
672             // for example: 'a.b.jpg' -> 'a_b.jpg'
673             //              'abc.jpg' NOT CHANGED
674             char[] c = filename.toCharArray();
675             boolean firstDot = true;
676             boolean modified = false;
677             for (int i = c.length - 1; i >= 0; i--) {
678                 if (c[i] == '.') {
679                     if (!firstDot) {
680                         modified = true;
681                         c[i] = '_';
682                     }
683                     firstDot = false;
684                 }
685             }
686 
687             if (modified) {
688                 String newFilename = new String(c);
689                 request.setHeader(HeaderSet.NAME, newFilename);
690                 Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename
691                         + "\" to workaround Poloroid filename quirk");
692             }
693         }
694     }
695 
696     @Override
unblock()697     public void unblock() {
698         // Not used for client case
699     }
700 
701 }
702