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