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