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