BluetoothPbapCallLogComposer.java revision ff55fbfa8cf30fe8a72e1a68420cd744ce957828
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 android.content.ContentResolver; 19import android.content.Context; 20import android.database.Cursor; 21import android.database.sqlite.SQLiteException; 22import android.net.Uri; 23import android.pim.vcard.VCardBuilder; 24import android.pim.vcard.VCardConfig; 25import android.pim.vcard.VCardConstants; 26import android.pim.vcard.VCardUtils; 27import android.pim.vcard.VCardComposer.OneEntryHandler; 28import android.provider.CallLog; 29import android.provider.CallLog.Calls; 30import android.text.TextUtils; 31import android.text.format.Time; 32import android.util.Log; 33 34import java.util.ArrayList; 35import java.util.Arrays; 36import java.util.List; 37 38/** 39 * VCard composer especially for Call Log used in Bluetooth. 40 */ 41public 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 // We should not allow vCard composer to re-format phone numbers, since 189 // some characters are (inappropriately) removed and devices do not work fine. 190 final int vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 | 191 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 192 final VCardBuilder builder = new VCardBuilder(vcardType); 193 String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); 194 if (TextUtils.isEmpty(name)) { 195 name = mCursor.getString(NUMBER_COLUMN_INDEX); 196 } 197 final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); 198 builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false); 199 builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false); 200 201 final String number = mCursor.getString(NUMBER_COLUMN_INDEX); 202 final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); 203 String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); 204 if (TextUtils.isEmpty(label)) { 205 label = Integer.toString(type); 206 } 207 builder.appendTelLine(type, label, number, false); 208 tryAppendCallHistoryTimeStampField(builder); 209 210 return builder.toString(); 211 } 212 213 /** 214 * This static function is to compose vCard for phone own number 215 */ 216 public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, 217 String phoneNumber, boolean vcardVer21) { 218 final int vcardType = (vcardVer21 ? 219 VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 : 220 VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8) | 221 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 222 final VCardBuilder builder = new VCardBuilder(vcardType); 223 boolean needCharset = false; 224 if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { 225 needCharset = true; 226 } 227 builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false); 228 builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false); 229 230 if (!TextUtils.isEmpty(phoneNumber)) { 231 String label = Integer.toString(phonetype); 232 builder.appendTelLine(phonetype, label, phoneNumber, false); 233 } 234 235 return builder.toString(); 236 } 237 238 /** 239 * Format according to RFC 2445 DATETIME type. 240 * The format is: ("%Y%m%dT%H%M%SZ"). 241 */ 242 private final String toRfc2455Format(final long millSecs) { 243 Time startDate = new Time(); 244 startDate.set(millSecs); 245 String date = startDate.format2445(); 246 return date + FLAG_TIMEZONE_UTC; 247 } 248 249 /** 250 * Try to append the property line for a call history time stamp field if possible. 251 * Do nothing if the call log type gotton from the database is invalid. 252 */ 253 private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) { 254 // Extension for call history as defined in 255 // in the Specification for Ic Mobile Communcation - ver 1.1, 256 // Oct 2000. This is used to send the details of the call 257 // history - missed, incoming, outgoing along with date and time 258 // to the requesting device (For example, transferring phone book 259 // when connected over bluetooth) 260 // 261 // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" 262 final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); 263 final String callLogTypeStr; 264 switch (callLogType) { 265 case Calls.INCOMING_TYPE: { 266 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; 267 break; 268 } 269 case Calls.OUTGOING_TYPE: { 270 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; 271 break; 272 } 273 case Calls.MISSED_TYPE: { 274 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; 275 break; 276 } 277 default: { 278 Log.w(TAG, "Call log type not correct."); 279 return; 280 } 281 } 282 283 final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); 284 builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP, 285 Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong)); 286 } 287 288 public void terminate() { 289 for (OneEntryHandler handler : mHandlerList) { 290 handler.onTerminate(); 291 } 292 293 if (mCursor != null) { 294 try { 295 mCursor.close(); 296 } catch (SQLiteException e) { 297 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 298 } 299 mCursor = null; 300 } 301 302 mTerminateIsCalled = true; 303 } 304 305 @Override 306 public void finalize() { 307 if (!mTerminateIsCalled) { 308 terminate(); 309 } 310 } 311 312 public int getCount() { 313 if (mCursor == null) { 314 return 0; 315 } 316 return mCursor.getCount(); 317 } 318 319 public boolean isAfterLast() { 320 if (mCursor == null) { 321 return false; 322 } 323 return mCursor.isAfterLast(); 324 } 325 326 public String getErrorReason() { 327 return mErrorReason; 328 } 329} 330