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