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