• 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 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.pim.vcard.VCardBuilder;
26 import android.pim.vcard.VCardConfig;
27 import android.pim.vcard.VCardConstants;
28 import android.pim.vcard.VCardUtils;
29 import android.pim.vcard.VCardComposer.OneEntryHandler;
30 import android.provider.CallLog;
31 import android.provider.CallLog.Calls;
32 import android.text.TextUtils;
33 import android.text.format.Time;
34 import android.util.Log;
35 
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.List;
39 
40 /**
41  * VCard composer especially for Call Log used in Bluetooth.
42  */
43 public class BluetoothPbapCallLogComposer {
44     private static final String TAG = "CallLogComposer";
45 
46     private static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
47         "Failed to get database information";
48 
49     private static final String FAILURE_REASON_NO_ENTRY =
50         "There's no exportable in the database";
51 
52     private static final String FAILURE_REASON_NOT_INITIALIZED =
53         "The vCard composer object is not correctly initialized";
54 
55     /** Should be visible only from developers... (no need to translate, hopefully) */
56     private static final String FAILURE_REASON_UNSUPPORTED_URI =
57         "The Uri vCard composer received is not supported by the composer.";
58 
59     private static final String NO_ERROR = "No error";
60 
61     /** The projection to use when querying the call log table */
62     private static final String[] sCallLogProjection = new String[] {
63             Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
64             Calls.CACHED_NUMBER_LABEL
65     };
66     private static final int NUMBER_COLUMN_INDEX = 0;
67     private static final int DATE_COLUMN_INDEX = 1;
68     private static final int CALL_TYPE_COLUMN_INDEX = 2;
69     private static final int CALLER_NAME_COLUMN_INDEX = 3;
70     private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
71     private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
72 
73     // Property for call log entry
74     private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
75     private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "RECEIVED";
76     private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "DIALED";
77     private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
78 
79     private static final String FLAG_TIMEZONE_UTC = "Z";
80 
81     private final Context mContext;
82     private ContentResolver mContentResolver;
83     private Cursor mCursor;
84     private final boolean mCareHandlerErrors;
85 
86     private boolean mTerminateIsCalled;
87     private final List<OneEntryHandler> mHandlerList;
88 
89     private String mErrorReason = NO_ERROR;
90 
BluetoothPbapCallLogComposer(final Context context, boolean careHandlerErrors)91     public BluetoothPbapCallLogComposer(final Context context, boolean careHandlerErrors) {
92         mContext = context;
93         mContentResolver = context.getContentResolver();
94         mCareHandlerErrors = careHandlerErrors;
95         mHandlerList = new ArrayList<OneEntryHandler>();
96     }
97 
init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)98     public boolean init(final Uri contentUri, final String selection,
99             final String[] selectionArgs, final String sortOrder) {
100         final String[] projection;
101         if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
102             projection = sCallLogProjection;
103         } else {
104             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
105             return false;
106         }
107 
108         mCursor = mContentResolver.query(
109                 contentUri, projection, selection, selectionArgs, sortOrder);
110 
111         if (mCursor == null) {
112             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
113             return false;
114         }
115 
116         if (mCareHandlerErrors) {
117             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
118                     mHandlerList.size());
119             for (OneEntryHandler handler : mHandlerList) {
120                 if (!handler.onInit(mContext)) {
121                     for (OneEntryHandler finished : finishedList) {
122                         finished.onTerminate();
123                     }
124                     return false;
125                 }
126             }
127         } else {
128             // Just ignore the false returned from onInit().
129             for (OneEntryHandler handler : mHandlerList) {
130                 handler.onInit(mContext);
131             }
132         }
133 
134         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
135             try {
136                 mCursor.close();
137             } catch (SQLiteException e) {
138                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
139             } finally {
140                 mErrorReason = FAILURE_REASON_NO_ENTRY;
141                 mCursor = null;
142             }
143             return false;
144         }
145 
146         return true;
147     }
148 
addHandler(OneEntryHandler handler)149     public void addHandler(OneEntryHandler handler) {
150         if (handler != null) {
151             mHandlerList.add(handler);
152         }
153     }
154 
createOneEntry()155     public boolean createOneEntry() {
156         if (mCursor == null || mCursor.isAfterLast()) {
157             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
158             return false;
159         }
160 
161         final String vcard;
162         try {
163             vcard = createOneCallLogEntryInternal();
164         } catch (OutOfMemoryError error) {
165             Log.e(TAG, "OutOfMemoryError occured. Ignore the entry");
166             System.gc();
167             return true;
168         } finally {
169             mCursor.moveToNext();
170         }
171 
172         if (mCareHandlerErrors) {
173             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
174                     mHandlerList.size());
175             for (OneEntryHandler handler : mHandlerList) {
176                 if (!handler.onEntryCreated(vcard)) {
177                     return false;
178                 }
179             }
180         } else {
181             for (OneEntryHandler handler : mHandlerList) {
182                 handler.onEntryCreated(vcard);
183             }
184         }
185 
186         return true;
187     }
188 
createOneCallLogEntryInternal()189     private String createOneCallLogEntryInternal() {
190         // We should not allow vCard composer to re-format phone numbers, since
191         // some characters are (inappropriately) removed and devices do not work fine.
192         final int vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC |
193                 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
194         final VCardBuilder builder = new VCardBuilder(vcardType);
195         String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
196         if (TextUtils.isEmpty(name)) {
197             name = mCursor.getString(NUMBER_COLUMN_INDEX);
198         }
199         final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
200         builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
201         builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
202 
203         String number = mCursor.getString(NUMBER_COLUMN_INDEX);
204         if (number.equals("-1")) {
205             number = mContext.getString(R.string.unknownNumber);
206         }
207         final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
208         String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
209         if (TextUtils.isEmpty(label)) {
210             label = Integer.toString(type);
211         }
212         builder.appendTelLine(type, label, number, false);
213         tryAppendCallHistoryTimeStampField(builder);
214 
215         return builder.toString();
216     }
217 
218     /**
219      * This static function is to compose vCard for phone own number
220      */
composeVCardForPhoneOwnNumber(int phonetype, String phoneName, String phoneNumber, boolean vcardVer21)221     public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
222             String phoneNumber, boolean vcardVer21) {
223         final int vcardType = (vcardVer21 ?
224                 VCardConfig.VCARD_TYPE_V21_GENERIC :
225                     VCardConfig.VCARD_TYPE_V30_GENERIC) |
226                 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
227         final VCardBuilder builder = new VCardBuilder(vcardType);
228         boolean needCharset = false;
229         if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
230             needCharset = true;
231         }
232         builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
233         builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
234 
235         if (!TextUtils.isEmpty(phoneNumber)) {
236             String label = Integer.toString(phonetype);
237             builder.appendTelLine(phonetype, label, phoneNumber, false);
238         }
239 
240         return builder.toString();
241     }
242 
243     /**
244      * Format according to RFC 2445 DATETIME type.
245      * The format is: ("%Y%m%dT%H%M%SZ").
246      */
toRfc2455Format(final long millSecs)247     private final String toRfc2455Format(final long millSecs) {
248         Time startDate = new Time();
249         startDate.set(millSecs);
250         String date = startDate.format2445();
251         return date + FLAG_TIMEZONE_UTC;
252     }
253 
254     /**
255      * Try to append the property line for a call history time stamp field if possible.
256      * Do nothing if the call log type gotton from the database is invalid.
257      */
tryAppendCallHistoryTimeStampField(final VCardBuilder builder)258     private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
259         // Extension for call history as defined in
260         // in the Specification for Ic Mobile Communcation - ver 1.1,
261         // Oct 2000. This is used to send the details of the call
262         // history - missed, incoming, outgoing along with date and time
263         // to the requesting device (For example, transferring phone book
264         // when connected over bluetooth)
265         //
266         // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
267         final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
268         final String callLogTypeStr;
269         switch (callLogType) {
270             case Calls.INCOMING_TYPE: {
271                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
272                 break;
273             }
274             case Calls.OUTGOING_TYPE: {
275                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
276                 break;
277             }
278             case Calls.MISSED_TYPE: {
279                 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
280                 break;
281             }
282             default: {
283                 Log.w(TAG, "Call log type not correct.");
284                 return;
285             }
286         }
287 
288         final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
289         builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
290                 Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
291     }
292 
terminate()293     public void terminate() {
294         for (OneEntryHandler handler : mHandlerList) {
295             handler.onTerminate();
296         }
297 
298         if (mCursor != null) {
299             try {
300                 mCursor.close();
301             } catch (SQLiteException e) {
302                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
303             }
304             mCursor = null;
305         }
306 
307         mTerminateIsCalled = true;
308     }
309 
310     @Override
finalize()311     public void finalize() {
312         if (!mTerminateIsCalled) {
313             terminate();
314         }
315     }
316 
getCount()317     public int getCount() {
318         if (mCursor == null) {
319             return 0;
320         }
321         return mCursor.getCount();
322     }
323 
isAfterLast()324     public boolean isAfterLast() {
325         if (mCursor == null) {
326             return false;
327         }
328         return mCursor.isAfterLast();
329     }
330 
getErrorReason()331     public String getErrorReason() {
332         return mErrorReason;
333     }
334 }
335