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