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