BluetoothPbapVcardManager.java revision 86d30be8aa68d0a2b561c36b53f4e5ebe586fe72
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
33package com.android.bluetooth.pbap;
34
35import android.content.ContentResolver;
36import android.content.Context;
37import android.database.Cursor;
38import android.net.Uri;
39import android.provider.CallLog;
40import android.provider.CallLog.Calls;
41import android.provider.ContactsContract.CommonDataKinds;
42import android.provider.ContactsContract.Contacts;
43import android.provider.ContactsContract.Data;
44import android.provider.ContactsContract.CommonDataKinds.Phone;
45import android.provider.ContactsContract.PhoneLookup;
46import android.text.TextUtils;
47import android.util.Log;
48
49import com.android.bluetooth.R;
50import com.android.internal.telephony.CallerInfo;
51import com.android.vcard.VCardComposer;
52import com.android.vcard.VCardConfig;
53
54import java.io.IOException;
55import java.io.OutputStream;
56import java.util.ArrayList;
57
58import javax.obex.ServerOperation;
59import javax.obex.Operation;
60import javax.obex.ResponseCodes;
61
62public class BluetoothPbapVcardManager {
63    private static final String TAG = "BluetoothPbapVcardManager";
64
65    private static final boolean V = BluetoothPbapService.VERBOSE;
66
67    private ContentResolver mResolver;
68
69    private Context mContext;
70
71    private StringBuilder mVcardResults = null;
72
73    static final String[] PHONES_PROJECTION = new String[] {
74            Data._ID, // 0
75            CommonDataKinds.Phone.TYPE, // 1
76            CommonDataKinds.Phone.LABEL, // 2
77            CommonDataKinds.Phone.NUMBER, // 3
78            Contacts.DISPLAY_NAME, // 4
79    };
80
81    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
82
83    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
84
85    static final String[] CONTACTS_PROJECTION = new String[] {
86            Contacts._ID, // 0
87            Contacts.DISPLAY_NAME, // 1
88    };
89
90    static final int CONTACTS_ID_COLUMN_INDEX = 0;
91
92    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
93
94    // call histories use dynamic handles, and handles should order by date; the
95    // most recently one should be the first handle. In table "calls", _id and
96    // date are consistent in ordering, to implement simply, we sort by _id
97    // here.
98    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
99
100    private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
101
102    public BluetoothPbapVcardManager(final Context context) {
103        mContext = context;
104        mResolver = mContext.getContentResolver();
105    }
106
107    public final String getOwnerPhoneNumberVcard(final boolean vcardType21) {
108        BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext);
109        String name = BluetoothPbapService.getLocalPhoneName();
110        String number = BluetoothPbapService.getLocalPhoneNum();
111        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
112                vcardType21);
113        return vcard;
114    }
115
116    public final int getPhonebookSize(final int type) {
117        int size;
118        switch (type) {
119            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
120                size = getContactsSize();
121                break;
122            default:
123                size = getCallHistorySize(type);
124                break;
125        }
126        if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type);
127        return size;
128    }
129
130    public final int getContactsSize() {
131        final Uri myUri = Contacts.CONTENT_URI;
132        int size = 0;
133        Cursor contactCursor = null;
134        try {
135            contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null);
136            if (contactCursor != null) {
137                size = contactCursor.getCount() + 1; // always has the 0.vcf
138            }
139        } finally {
140            if (contactCursor != null) {
141                contactCursor.close();
142            }
143        }
144        return size;
145    }
146
147    public final int getCallHistorySize(final int type) {
148        final Uri myUri = CallLog.Calls.CONTENT_URI;
149        String selection = BluetoothPbapObexServer.createSelectionPara(type);
150        int size = 0;
151        Cursor callCursor = null;
152        try {
153            callCursor = mResolver.query(myUri, null, selection, null,
154                    CallLog.Calls.DEFAULT_SORT_ORDER);
155            if (callCursor != null) {
156                size = callCursor.getCount();
157            }
158        } finally {
159            if (callCursor != null) {
160                callCursor.close();
161            }
162        }
163        return size;
164    }
165
166    public final ArrayList<String> loadCallHistoryList(final int type) {
167        final Uri myUri = CallLog.Calls.CONTENT_URI;
168        String selection = BluetoothPbapObexServer.createSelectionPara(type);
169        String[] projection = new String[] {
170                Calls.NUMBER, Calls.CACHED_NAME
171        };
172        final int CALLS_NUMBER_COLUMN_INDEX = 0;
173        final int CALLS_NAME_COLUMN_INDEX = 1;
174
175        Cursor callCursor = null;
176        ArrayList<String> list = new ArrayList<String>();
177        try {
178            callCursor = mResolver.query(myUri, projection, selection, null,
179                    CALLLOG_SORT_ORDER);
180            if (callCursor != null) {
181                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
182                        callCursor.moveToNext()) {
183                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
184                    if (TextUtils.isEmpty(name)) {
185                        // name not found, use number instead
186                        name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
187                        if (CallerInfo.UNKNOWN_NUMBER.equals(name) ||
188                                CallerInfo.PRIVATE_NUMBER.equals(name) ||
189                                CallerInfo.PAYPHONE_NUMBER.equals(name)) {
190                            name = mContext.getString(R.string.unknownNumber);
191                        }
192                    }
193                    list.add(name);
194                }
195            }
196        } finally {
197            if (callCursor != null) {
198                callCursor.close();
199            }
200        }
201        return list;
202    }
203
204    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
205        ArrayList<String> nameList = new ArrayList<String>();
206        nameList.add(BluetoothPbapService.getLocalPhoneName());
207
208        final Uri myUri = Contacts.CONTENT_URI;
209        Cursor contactCursor = null;
210        try {
211            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
212                if (V) Log.v(TAG, "getPhonebookNameList, order by index");
213                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
214                        null, Contacts._ID);
215            } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
216                if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
217                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
218                        null, Contacts.DISPLAY_NAME);
219            }
220            if (contactCursor != null) {
221                for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
222                        .moveToNext()) {
223                    String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
224                    if (TextUtils.isEmpty(name)) {
225                        name = mContext.getString(android.R.string.unknownName);
226                    }
227                    nameList.add(name);
228                }
229            }
230        } finally {
231            if (contactCursor != null) {
232                contactCursor.close();
233            }
234        }
235        return nameList;
236    }
237
238    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
239        ArrayList<String> nameList = new ArrayList<String>();
240
241        Cursor contactCursor = null;
242        Uri uri = null;
243
244        if (phoneNumber != null && phoneNumber.length() == 0) {
245            uri = Contacts.CONTENT_URI;
246        } else {
247            uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
248                Uri.encode(phoneNumber));
249        }
250
251        try {
252            contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
253                        null, Contacts._ID);
254
255            if (contactCursor != null) {
256                for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
257                        .moveToNext()) {
258                    String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
259                    long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
260                    if (TextUtils.isEmpty(name)) {
261                        name = mContext.getString(android.R.string.unknownName);
262                    }
263                    if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id);
264                    nameList.add(name);
265                }
266            }
267        } finally {
268            if (contactCursor != null) {
269                contactCursor.close();
270            }
271        }
272        return nameList;
273    }
274
275    public final int composeAndSendCallLogVcards(final int type, Operation op,
276            final int startPoint, final int endPoint, final boolean vcardType21) {
277        if (startPoint < 1 || startPoint > endPoint) {
278            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
279            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
280        }
281        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
282
283        final Uri myUri = CallLog.Calls.CONTENT_URI;
284        final String[] CALLLOG_PROJECTION = new String[] {
285            CallLog.Calls._ID, // 0
286        };
287        final int ID_COLUMN_INDEX = 0;
288
289        Cursor callsCursor = null;
290        long startPointId = 0;
291        long endPointId = 0;
292        try {
293            // Need test to see if order by _ID is ok here, or by date?
294            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
295                    CALLLOG_SORT_ORDER);
296            if (callsCursor != null) {
297                callsCursor.moveToPosition(startPoint - 1);
298                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
299                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
300                if (startPoint == endPoint) {
301                    endPointId = startPointId;
302                } else {
303                    callsCursor.moveToPosition(endPoint - 1);
304                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
305                }
306                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
307            }
308        } finally {
309            if (callsCursor != null) {
310                callsCursor.close();
311            }
312        }
313
314        String recordSelection;
315        if (startPoint == endPoint) {
316            recordSelection = Calls._ID + "=" + startPointId;
317        } else {
318            // The query to call table is by "_id DESC" order, so change
319            // correspondingly.
320            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
321                    + startPointId;
322        }
323
324        String selection;
325        if (typeSelection == null) {
326            selection = recordSelection;
327        } else {
328            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
329        }
330
331        if (V) Log.v(TAG, "Call log query selection is: " + selection);
332
333        return composeAndSendVCards(op, selection, vcardType21, null, false);
334    }
335
336    public final int composeAndSendPhonebookVcards(Operation op, final int startPoint,
337            final int endPoint, final boolean vcardType21, String ownerVCard) {
338        if (startPoint < 1 || startPoint > endPoint) {
339            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
340            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
341        }
342        final Uri myUri = Contacts.CONTENT_URI;
343
344        Cursor contactCursor = null;
345        long startPointId = 0;
346        long endPointId = 0;
347        try {
348            contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null,
349                    Contacts._ID);
350            if (contactCursor != null) {
351                contactCursor.moveToPosition(startPoint - 1);
352                startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
353                if (V) Log.v(TAG, "Query startPointId = " + startPointId);
354                if (startPoint == endPoint) {
355                    endPointId = startPointId;
356                } else {
357                    contactCursor.moveToPosition(endPoint - 1);
358                    endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
359                }
360                if (V) Log.v(TAG, "Query endPointId = " + endPointId);
361            }
362        } finally {
363            if (contactCursor != null) {
364                contactCursor.close();
365            }
366        }
367
368        final String selection;
369        if (startPoint == endPoint) {
370            selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE;
371        } else {
372            selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<="
373                    + endPointId + " AND " + CLAUSE_ONLY_VISIBLE;
374        }
375
376        if (V) Log.v(TAG, "Query selection is: " + selection);
377
378        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
379    }
380
381    public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
382            final boolean vcardType21, String ownerVCard, int orderByWhat) {
383        if (offset < 1) {
384            Log.e(TAG, "Internal error: offset is not correct.");
385            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
386        }
387        final Uri myUri = Contacts.CONTENT_URI;
388        Cursor contactCursor = null;
389        String selection = null;
390        long contactId = 0;
391        if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
392            try {
393                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
394                        null, Contacts._ID);
395                if (contactCursor != null) {
396                    contactCursor.moveToPosition(offset - 1);
397                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
398                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
399                }
400            } finally {
401                if (contactCursor != null) {
402                    contactCursor.close();
403                }
404            }
405        } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
406            try {
407                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
408                        null, Contacts.DISPLAY_NAME);
409                if (contactCursor != null) {
410                    contactCursor.moveToPosition(offset - 1);
411                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
412                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
413                }
414            } finally {
415                if (contactCursor != null) {
416                    contactCursor.close();
417                }
418            }
419        } else {
420            Log.e(TAG, "Parameter orderByWhat is not supported!");
421            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
422        }
423        selection = Contacts._ID + "=" + contactId;
424
425        if (V) Log.v(TAG, "Query selection is: " + selection);
426
427        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
428    }
429
430    public final int composeAndSendVCards(Operation op, final String selection,
431            final boolean vcardType21, String ownerVCard, boolean isContacts) {
432        long timestamp = 0;
433        if (V) timestamp = System.currentTimeMillis();
434
435        if (isContacts) {
436            VCardComposer composer = null;
437            HandlerForStringBuffer buffer = null;
438            try {
439                // Currently only support Generic Vcard 2.1 and 3.0
440                int vcardType;
441                if (vcardType21) {
442                    vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
443                } else {
444                    vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
445                }
446                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
447                vcardType |= VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
448
449                composer = new VCardComposer(mContext, vcardType, true);
450                buffer = new HandlerForStringBuffer(op, ownerVCard);
451                if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID) ||
452                        !buffer.onInit(mContext)) {
453                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
454                }
455
456                while (!composer.isAfterLast()) {
457                    if (BluetoothPbapObexServer.sIsAborted) {
458                        ((ServerOperation)op).isAborted = true;
459                        BluetoothPbapObexServer.sIsAborted = false;
460                        break;
461                    }
462                    String vcard = composer.createOneEntry();
463                    if (vcard == null) {
464                        Log.e(TAG, "Failed to read a contact. Error reason: "
465                                + composer.getErrorReason());
466                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
467                    }
468                    if (!buffer.onEntryCreated(vcard)) {
469                        // onEntryCreate() already emits error.
470                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
471                    }
472                }
473            } finally {
474                if (composer != null) {
475                    composer.terminate();
476                }
477                if (buffer != null) {
478                    buffer.onTerminate();
479                }
480            }
481        } else { // CallLog
482            BluetoothPbapCallLogComposer composer = null;
483            HandlerForStringBuffer buffer = null;
484            try {
485
486                composer = new BluetoothPbapCallLogComposer(mContext);
487                buffer = new HandlerForStringBuffer(op, ownerVCard);
488                if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null,
489                                   CALLLOG_SORT_ORDER) ||
490                                   !buffer.onInit(mContext)) {
491                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
492                }
493
494                while (!composer.isAfterLast()) {
495                    if (BluetoothPbapObexServer.sIsAborted) {
496                        ((ServerOperation)op).isAborted = true;
497                        BluetoothPbapObexServer.sIsAborted = false;
498                        break;
499                    }
500                    String vcard = composer.createOneEntry(vcardType21);
501                    if (vcard == null) {
502                        Log.e(TAG, "Failed to read a contact. Error reason: "
503                                + composer.getErrorReason());
504                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
505                    }
506                    buffer.onEntryCreated(vcard);
507                }
508            } finally {
509                if (composer != null) {
510                    composer.terminate();
511                }
512                if (buffer != null) {
513                    buffer.onTerminate();
514                }
515            }
516        }
517
518        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
519                    + (System.currentTimeMillis() - timestamp) + " ms");
520
521        return ResponseCodes.OBEX_HTTP_OK;
522    }
523
524    /**
525     * Handler to emit VCard String to PCE once size grow to maxPacketSize.
526     */
527    public class HandlerForStringBuffer {
528        private Operation operation;
529
530        private OutputStream outputStream;
531
532        private int maxPacketSize;
533
534        private String phoneOwnVCard = null;
535
536        public HandlerForStringBuffer(Operation op, String ownerVCard) {
537            operation = op;
538            maxPacketSize = operation.getMaxPacketSize();
539            if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize);
540            if (ownerVCard != null) {
541                phoneOwnVCard = ownerVCard;
542                if (V) Log.v(TAG, "phone own number vcard:");
543                if (V) Log.v(TAG, phoneOwnVCard);
544            }
545        }
546
547        public boolean onInit(Context context) {
548            try {
549                outputStream = operation.openOutputStream();
550                mVcardResults = new StringBuilder();
551                if (phoneOwnVCard != null) {
552                    mVcardResults.append(phoneOwnVCard);
553                }
554            } catch (IOException e) {
555                Log.e(TAG, "open outputstrem failed" + e.toString());
556                return false;
557            }
558            if (V) Log.v(TAG, "openOutputStream() ok.");
559            return true;
560        }
561
562        public boolean onEntryCreated(String vcard) {
563            int vcardLen = vcard.length();
564            if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen);
565
566            mVcardResults.append(vcard);
567            int vcardByteLen = mVcardResults.toString().getBytes().length;
568            if (V) Log.v(TAG, "The byte length of this vcardResults is: " + vcardByteLen);
569
570            if (vcardByteLen >= maxPacketSize) {
571                long timestamp = 0;
572                int position = 0;
573
574                // Need while loop to handle the big vcard case
575                while (!BluetoothPbapObexServer.sIsAborted
576                        && position < (vcardByteLen - maxPacketSize)) {
577                    if (V) timestamp = System.currentTimeMillis();
578
579                    String subStr = mVcardResults.toString().substring(position,
580                            position + maxPacketSize);
581                    try {
582                        outputStream.write(subStr.getBytes(), 0, maxPacketSize);
583                    } catch (IOException e) {
584                        Log.e(TAG, "write outputstrem failed" + e.toString());
585                        return false;
586                    }
587                    if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took "
588                            + (System.currentTimeMillis() - timestamp) + " ms");
589
590                    position += maxPacketSize;
591                }
592                mVcardResults.delete(0, position);
593            }
594            return true;
595        }
596
597        public void onTerminate() {
598            // Send out last packet
599            byte[] lastBytes = mVcardResults.toString().getBytes();
600            try {
601                outputStream.write(lastBytes, 0, lastBytes.length);
602            } catch (IOException e) {
603                Log.e(TAG, "write outputstrem failed" + e.toString());
604            }
605            if (V) Log.v(TAG, "Last packet sent out, sending process complete!");
606
607            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
608                if (V) Log.v(TAG, "CloseStream failed!");
609            } else {
610                if (V) Log.v(TAG, "CloseStream ok!");
611            }
612        }
613    }
614}
615