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.provider.CallLog; 24 import android.provider.CallLog.Calls; 25 import android.text.TextUtils; 26 import android.util.Log; 27 28 import com.android.bluetooth.R; 29 import com.android.vcard.VCardBuilder; 30 import com.android.vcard.VCardConfig; 31 import com.android.vcard.VCardConstants; 32 import com.android.vcard.VCardUtils; 33 34 import java.text.SimpleDateFormat; 35 import java.util.Arrays; 36 import java.util.Calendar; 37 38 /** 39 * VCard composer especially for Call Log used in Bluetooth. 40 */ 41 public class BluetoothPbapCallLogComposer { 42 private static final String TAG = "PbapCallLogComposer"; 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 = "There's no exportable in the database"; 48 49 private static final String FAILURE_REASON_NOT_INITIALIZED = 50 "The vCard composer object is not correctly initialized"; 51 52 /** Should be visible only from developers... (no need to translate, hopefully) */ 53 private static final String FAILURE_REASON_UNSUPPORTED_URI = 54 "The Uri vCard composer received is not supported by the composer."; 55 56 private static final String NO_ERROR = "No error"; 57 58 /** The projection to use when querying the call log table */ 59 private static final String[] sCallLogProjection = new String[]{ 60 Calls.NUMBER, 61 Calls.DATE, 62 Calls.TYPE, 63 Calls.CACHED_NAME, 64 Calls.CACHED_NUMBER_TYPE, 65 Calls.CACHED_NUMBER_LABEL, 66 Calls.NUMBER_PRESENTATION 67 }; 68 private static final int NUMBER_COLUMN_INDEX = 0; 69 private static final int DATE_COLUMN_INDEX = 1; 70 private static final int CALL_TYPE_COLUMN_INDEX = 2; 71 private static final int CALLER_NAME_COLUMN_INDEX = 3; 72 private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; 73 private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; 74 private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; 75 76 // Property for call log entry 77 private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; 78 private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "RECEIVED"; 79 private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "DIALED"; 80 private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; 81 82 private final Context mContext; 83 private ContentResolver mContentResolver; 84 private Cursor mCursor; 85 86 private boolean mTerminateIsCalled; 87 88 private String mErrorReason = NO_ERROR; 89 90 private final String RFC_2455_FORMAT = "yyyyMMdd'T'HHmmss"; 91 BluetoothPbapCallLogComposer(final Context context)92 public BluetoothPbapCallLogComposer(final Context context) { 93 mContext = context; 94 mContentResolver = context.getContentResolver(); 95 } 96 init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)97 public boolean init(final Uri contentUri, final String selection, final String[] selectionArgs, 98 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 = 108 mContentResolver.query(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 (mCursor.getCount() == 0 || !mCursor.moveToFirst()) { 116 try { 117 mCursor.close(); 118 } catch (SQLiteException e) { 119 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 120 } finally { 121 mErrorReason = FAILURE_REASON_NO_ENTRY; 122 mCursor = null; 123 } 124 return false; 125 } 126 127 return true; 128 } 129 createOneEntry(boolean vcardVer21)130 public String createOneEntry(boolean vcardVer21) { 131 if (mCursor == null || mCursor.isAfterLast()) { 132 mErrorReason = FAILURE_REASON_NOT_INITIALIZED; 133 return null; 134 } 135 try { 136 return createOneCallLogEntryInternal(vcardVer21); 137 } finally { 138 mCursor.moveToNext(); 139 } 140 } 141 createOneCallLogEntryInternal(boolean vcardVer21)142 private String createOneCallLogEntryInternal(boolean vcardVer21) { 143 final int vcardType = (vcardVer21 ? VCardConfig.VCARD_TYPE_V21_GENERIC 144 : VCardConfig.VCARD_TYPE_V30_GENERIC) 145 | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 146 final VCardBuilder builder = new VCardBuilder(vcardType); 147 String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); 148 String number = mCursor.getString(NUMBER_COLUMN_INDEX); 149 final int numberPresentation = mCursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX); 150 if (TextUtils.isEmpty(name)) { 151 name = ""; 152 } 153 if (numberPresentation != Calls.PRESENTATION_ALLOWED) { 154 // setting name to "" as FN/N must be empty fields in this case. 155 name = ""; 156 // TODO: there are really 3 possible strings that could be set here: 157 // "unknown", "private", and "payphone". 158 number = mContext.getString(R.string.unknownNumber); 159 } 160 final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); 161 builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false); 162 builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false); 163 164 final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); 165 String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); 166 if (TextUtils.isEmpty(label)) { 167 label = Integer.toString(type); 168 } 169 builder.appendTelLine(type, label, number, false); 170 tryAppendCallHistoryTimeStampField(builder); 171 172 return builder.toString(); 173 } 174 175 /** 176 * This static function is to compose vCard for phone own number 177 */ composeVCardForPhoneOwnNumber(int phonetype, String phoneName, String phoneNumber, boolean vcardVer21)178 public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, String phoneNumber, 179 boolean vcardVer21) { 180 final int vcardType = (vcardVer21 ? VCardConfig.VCARD_TYPE_V21_GENERIC 181 : VCardConfig.VCARD_TYPE_V30_GENERIC) 182 | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 183 final VCardBuilder builder = new VCardBuilder(vcardType); 184 boolean needCharset = false; 185 if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { 186 needCharset = true; 187 } 188 builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false); 189 builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false); 190 191 if (!TextUtils.isEmpty(phoneNumber)) { 192 String label = Integer.toString(phonetype); 193 builder.appendTelLine(phonetype, label, phoneNumber, false); 194 } 195 196 return builder.toString(); 197 } 198 199 /** 200 * Format according to RFC 2445 DATETIME type. 201 * The format is: ("%Y%m%dT%H%M%S"). 202 */ toRfc2455Format(final long millSecs)203 private String toRfc2455Format(final long millSecs) { 204 Calendar cal = Calendar.getInstance(); 205 cal.setTimeInMillis(millSecs); 206 SimpleDateFormat df = new SimpleDateFormat(RFC_2455_FORMAT); 207 return df.format(cal.getTime()); 208 } 209 210 /** 211 * Try to append the property line for a call history time stamp field if possible. 212 * Do nothing if the call log type gotton from the database is invalid. 213 */ tryAppendCallHistoryTimeStampField(final VCardBuilder builder)214 private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) { 215 // Extension for call history as defined in 216 // in the Specification for Ic Mobile Communcation - ver 1.1, 217 // Oct 2000. This is used to send the details of the call 218 // history - missed, incoming, outgoing along with date and time 219 // to the requesting device (For example, transferring phone book 220 // when connected over bluetooth) 221 // 222 // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000" 223 final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); 224 final String callLogTypeStr; 225 switch (callLogType) { 226 case Calls.REJECTED_TYPE: 227 case Calls.INCOMING_TYPE: { 228 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; 229 break; 230 } 231 case Calls.OUTGOING_TYPE: { 232 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; 233 break; 234 } 235 case Calls.MISSED_TYPE: { 236 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; 237 break; 238 } 239 default: { 240 Log.w(TAG, "Call log type not correct."); 241 return; 242 } 243 } 244 245 final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); 246 builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP, Arrays.asList(callLogTypeStr), 247 toRfc2455Format(dateAsLong)); 248 } 249 terminate()250 public void terminate() { 251 if (mCursor != null) { 252 try { 253 mCursor.close(); 254 } catch (SQLiteException e) { 255 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 256 } 257 mCursor = null; 258 } 259 260 mTerminateIsCalled = true; 261 } 262 263 @Override finalize()264 public void finalize() { 265 if (!mTerminateIsCalled) { 266 terminate(); 267 } 268 } 269 getCount()270 public int getCount() { 271 if (mCursor == null) { 272 return 0; 273 } 274 return mCursor.getCount(); 275 } 276 isAfterLast()277 public boolean isAfterLast() { 278 if (mCursor == null) { 279 return false; 280 } 281 return mCursor.isAfterLast(); 282 } 283 getErrorReason()284 public String getErrorReason() { 285 return mErrorReason; 286 } 287 } 288