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