Home | History | Annotate | Download | only in pbap
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package com.android.bluetooth.pbap;
     17 
     18 import com.android.bluetooth.R;
     19 
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.database.sqlite.SQLiteException;
     24 import android.net.Uri;
     25 import android.pim.vcard.VCardBuilder;
     26 import android.pim.vcard.VCardConfig;
     27 import android.pim.vcard.VCardConstants;
     28 import android.pim.vcard.VCardUtils;
     29 import android.pim.vcard.VCardComposer.OneEntryHandler;
     30 import android.provider.CallLog;
     31 import android.provider.CallLog.Calls;
     32 import android.text.TextUtils;
     33 import android.text.format.Time;
     34 import android.util.Log;
     35 
     36 import java.util.ArrayList;
     37 import java.util.Arrays;
     38 import java.util.List;
     39 
     40 /**
     41  * VCard composer especially for Call Log used in Bluetooth.
     42  */
     43 public class BluetoothPbapCallLogComposer {
     44     private static final String TAG = "CallLogComposer";
     45 
     46     private static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
     47         "Failed to get database information";
     48 
     49     private static final String FAILURE_REASON_NO_ENTRY =
     50         "There's no exportable in the database";
     51 
     52     private static final String FAILURE_REASON_NOT_INITIALIZED =
     53         "The vCard composer object is not correctly initialized";
     54 
     55     /** Should be visible only from developers... (no need to translate, hopefully) */
     56     private static final String FAILURE_REASON_UNSUPPORTED_URI =
     57         "The Uri vCard composer received is not supported by the composer.";
     58 
     59     private static final String NO_ERROR = "No error";
     60 
     61     /** The projection to use when querying the call log table */
     62     private static final String[] sCallLogProjection = new String[] {
     63             Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
     64             Calls.CACHED_NUMBER_LABEL
     65     };
     66     private static final int NUMBER_COLUMN_INDEX = 0;
     67     private static final int DATE_COLUMN_INDEX = 1;
     68     private static final int CALL_TYPE_COLUMN_INDEX = 2;
     69     private static final int CALLER_NAME_COLUMN_INDEX = 3;
     70     private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
     71     private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
     72 
     73     // Property for call log entry
     74     private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
     75     private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "RECEIVED";
     76     private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "DIALED";
     77     private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
     78 
     79     private static final String FLAG_TIMEZONE_UTC = "Z";
     80 
     81     private final Context mContext;
     82     private ContentResolver mContentResolver;
     83     private Cursor mCursor;
     84     private final boolean mCareHandlerErrors;
     85 
     86     private boolean mTerminateIsCalled;
     87     private final List<OneEntryHandler> mHandlerList;
     88 
     89     private String mErrorReason = NO_ERROR;
     90 
     91     public BluetoothPbapCallLogComposer(final Context context, boolean careHandlerErrors) {
     92         mContext = context;
     93         mContentResolver = context.getContentResolver();
     94         mCareHandlerErrors = careHandlerErrors;
     95         mHandlerList = new ArrayList<OneEntryHandler>();
     96     }
     97 
     98     public boolean init(final Uri contentUri, final String selection,
     99             final String[] selectionArgs, final String sortOrder) {
    100         final String[] projection;
    101         if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
    102             projection = sCallLogProjection;
    103         } else {
    104             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
    105             return false;
    106         }
    107 
    108         mCursor = mContentResolver.query(
    109                 contentUri, projection, selection, selectionArgs, sortOrder);
    110 
    111         if (mCursor == null) {
    112             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
    113             return false;
    114         }
    115 
    116         if (mCareHandlerErrors) {
    117             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    118                     mHandlerList.size());
    119             for (OneEntryHandler handler : mHandlerList) {
    120                 if (!handler.onInit(mContext)) {
    121                     for (OneEntryHandler finished : finishedList) {
    122                         finished.onTerminate();
    123                     }
    124                     return false;
    125                 }
    126             }
    127         } else {
    128             // Just ignore the false returned from onInit().
    129             for (OneEntryHandler handler : mHandlerList) {
    130                 handler.onInit(mContext);
    131             }
    132         }
    133 
    134         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
    135             try {
    136                 mCursor.close();
    137             } catch (SQLiteException e) {
    138                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    139             } finally {
    140                 mErrorReason = FAILURE_REASON_NO_ENTRY;
    141                 mCursor = null;
    142             }
    143             return false;
    144         }
    145 
    146         return true;
    147     }
    148 
    149     public void addHandler(OneEntryHandler handler) {
    150         if (handler != null) {
    151             mHandlerList.add(handler);
    152         }
    153     }
    154 
    155     public boolean createOneEntry() {
    156         if (mCursor == null || mCursor.isAfterLast()) {
    157             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
    158             return false;
    159         }
    160 
    161         final String vcard;
    162         try {
    163             vcard = createOneCallLogEntryInternal();
    164         } catch (OutOfMemoryError error) {
    165             Log.e(TAG, "OutOfMemoryError occured. Ignore the entry");
    166             System.gc();
    167             return true;
    168         } finally {
    169             mCursor.moveToNext();
    170         }
    171 
    172         if (mCareHandlerErrors) {
    173             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    174                     mHandlerList.size());
    175             for (OneEntryHandler handler : mHandlerList) {
    176                 if (!handler.onEntryCreated(vcard)) {
    177                     return false;
    178                 }
    179             }
    180         } else {
    181             for (OneEntryHandler handler : mHandlerList) {
    182                 handler.onEntryCreated(vcard);
    183             }
    184         }
    185 
    186         return true;
    187     }
    188 
    189     private String createOneCallLogEntryInternal() {
    190         // We should not allow vCard composer to re-format phone numbers, since
    191         // some characters are (inappropriately) removed and devices do not work fine.
    192         final int vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC |
    193                 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
    194         final VCardBuilder builder = new VCardBuilder(vcardType);
    195         String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
    196         if (TextUtils.isEmpty(name)) {
    197             name = mCursor.getString(NUMBER_COLUMN_INDEX);
    198         }
    199         final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
    200         builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
    201         builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
    202 
    203         String number = mCursor.getString(NUMBER_COLUMN_INDEX);
    204         if (number.equals("-1")) {
    205             number = mContext.getString(R.string.unknownNumber);
    206         }
    207         final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
    208         String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
    209         if (TextUtils.isEmpty(label)) {
    210             label = Integer.toString(type);
    211         }
    212         builder.appendTelLine(type, label, number, false);
    213         tryAppendCallHistoryTimeStampField(builder);
    214 
    215         return builder.toString();
    216     }
    217 
    218     /**
    219      * This static function is to compose vCard for phone own number
    220      */
    221     public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
    222             String phoneNumber, boolean vcardVer21) {
    223         final int vcardType = (vcardVer21 ?
    224                 VCardConfig.VCARD_TYPE_V21_GENERIC :
    225                     VCardConfig.VCARD_TYPE_V30_GENERIC) |
    226                 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
    227         final VCardBuilder builder = new VCardBuilder(vcardType);
    228         boolean needCharset = false;
    229         if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
    230             needCharset = true;
    231         }
    232         builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
    233         builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
    234 
    235         if (!TextUtils.isEmpty(phoneNumber)) {
    236             String label = Integer.toString(phonetype);
    237             builder.appendTelLine(phonetype, label, phoneNumber, false);
    238         }
    239 
    240         return builder.toString();
    241     }
    242 
    243     /**
    244      * Format according to RFC 2445 DATETIME type.
    245      * The format is: ("%Y%m%dT%H%M%SZ").
    246      */
    247     private final String toRfc2455Format(final long millSecs) {
    248         Time startDate = new Time();
    249         startDate.set(millSecs);
    250         String date = startDate.format2445();
    251         return date + FLAG_TIMEZONE_UTC;
    252     }
    253 
    254     /**
    255      * Try to append the property line for a call history time stamp field if possible.
    256      * Do nothing if the call log type gotton from the database is invalid.
    257      */
    258     private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
    259         // Extension for call history as defined in
    260         // in the Specification for Ic Mobile Communcation - ver 1.1,
    261         // Oct 2000. This is used to send the details of the call
    262         // history - missed, incoming, outgoing along with date and time
    263         // to the requesting device (For example, transferring phone book
    264         // when connected over bluetooth)
    265         //
    266         // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
    267         final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
    268         final String callLogTypeStr;
    269         switch (callLogType) {
    270             case Calls.INCOMING_TYPE: {
    271                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
    272                 break;
    273             }
    274             case Calls.OUTGOING_TYPE: {
    275                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
    276                 break;
    277             }
    278             case Calls.MISSED_TYPE: {
    279                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
    280                 break;
    281             }
    282             default: {
    283                 Log.w(TAG, "Call log type not correct.");
    284                 return;
    285             }
    286         }
    287 
    288         final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
    289         builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
    290                 Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
    291     }
    292 
    293     public void terminate() {
    294         for (OneEntryHandler handler : mHandlerList) {
    295             handler.onTerminate();
    296         }
    297 
    298         if (mCursor != null) {
    299             try {
    300                 mCursor.close();
    301             } catch (SQLiteException e) {
    302                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    303             }
    304             mCursor = null;
    305         }
    306 
    307         mTerminateIsCalled = true;
    308     }
    309 
    310     @Override
    311     public void finalize() {
    312         if (!mTerminateIsCalled) {
    313             terminate();
    314         }
    315     }
    316 
    317     public int getCount() {
    318         if (mCursor == null) {
    319             return 0;
    320         }
    321         return mCursor.getCount();
    322     }
    323 
    324     public boolean isAfterLast() {
    325         if (mCursor == null) {
    326             return false;
    327         }
    328         return mCursor.isAfterLast();
    329     }
    330 
    331     public String getErrorReason() {
    332         return mErrorReason;
    333     }
    334 }
    335