• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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