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