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