BluetoothPbapCallLogComposer.java revision 00aee7a70e3c76b6c9ef59133d59fcdc592fea99
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 */ 16package com.android.bluetooth.pbap; 17 18import com.android.bluetooth.R; 19 20import android.content.ContentResolver; 21import android.content.Context; 22import android.database.Cursor; 23import android.database.sqlite.SQLiteException; 24import android.net.Uri; 25import android.pim.vcard.VCardBuilder; 26import android.pim.vcard.VCardConfig; 27import android.pim.vcard.VCardConstants; 28import android.pim.vcard.VCardUtils; 29import android.pim.vcard.VCardComposer.OneEntryHandler; 30import android.provider.CallLog; 31import android.provider.CallLog.Calls; 32import android.text.TextUtils; 33import android.text.format.Time; 34import android.util.Log; 35 36import java.util.ArrayList; 37import java.util.Arrays; 38import java.util.List; 39 40/** 41 * VCard composer especially for Call Log used in Bluetooth. 42 */ 43public 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_UTF8 | 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_UTF8 : 225 VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8) | 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