1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.contacts.vcard;
17 
18 import android.app.Notification;
19 import android.app.NotificationManager;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.net.Uri;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.provider.ContactsContract.Contacts;
28 import android.provider.ContactsContract.RawContactsEntity;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.widget.Toast;
32 
33 import com.android.contacts.R;
34 import com.android.contactsbind.FeedbackHelper;
35 import com.android.vcard.VCardComposer;
36 import com.android.vcard.VCardConfig;
37 
38 import java.io.BufferedWriter;
39 import java.io.FileNotFoundException;
40 import java.io.IOException;
41 import java.io.OutputStream;
42 import java.io.OutputStreamWriter;
43 import java.io.Writer;
44 
45 /**
46  * Class for processing one export request from a user. Dropped after exporting requested Uri(s).
47  * {@link VCardService} will create another object when there is another export request.
48  */
49 public class ExportProcessor extends ProcessorBase {
50     private static final String LOG_TAG = "VCardExport";
51     private static final boolean DEBUG = VCardService.DEBUG;
52 
53     private final VCardService mService;
54     private final ContentResolver mResolver;
55     private final NotificationManager mNotificationManager;
56     private final ExportRequest mExportRequest;
57     private final int mJobId;
58     private final String mCallingActivity;
59 
60     private volatile boolean mCanceled;
61     private volatile boolean mDone;
62 
63     private final int SHOW_READY_TOAST = 1;
64     private final Handler handler = new Handler() {
65         public void handleMessage(Message msg) {
66             if (msg.arg1 == SHOW_READY_TOAST) {
67                 // This message is long, so we set the duration to LENGTH_LONG.
68                 Toast.makeText(mService,
69                         R.string.exporting_vcard_finished_toast, Toast.LENGTH_LONG).show();
70             }
71 
72         }
73     };
74 
ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId, String callingActivity)75     public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId,
76             String callingActivity) {
77         mService = service;
78         mResolver = service.getContentResolver();
79         mNotificationManager =
80                 (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
81         mExportRequest = exportRequest;
82         mJobId = jobId;
83         mCallingActivity = callingActivity;
84         try {
85             mResolver.takePersistableUriPermission(exportRequest.destUri,
86                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
87         } catch (SecurityException e) {
88             Log.w(LOG_TAG, "SecurityException error", e);
89         }
90     }
91 
92     @Override
getType()93     public final int getType() {
94         return VCardService.TYPE_EXPORT;
95     }
96 
97     @Override
run()98     public void run() {
99         // ExecutorService ignores RuntimeException, so we need to show it here.
100         try {
101             runInternal();
102 
103             if (isCancelled()) {
104                 doCancelNotification();
105             }
106         } catch (OutOfMemoryError|RuntimeException e) {
107             FeedbackHelper.sendFeedback(mService, LOG_TAG, "Failed to process vcard export", e);
108             throw e;
109         } finally {
110             synchronized (this) {
111                 mDone = true;
112             }
113         }
114     }
115 
runInternal()116     private void runInternal() {
117         if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
118         final ExportRequest request = mExportRequest;
119         VCardComposer composer = null;
120         Writer writer = null;
121         boolean successful = false;
122         try {
123             if (isCancelled()) {
124                 Log.i(LOG_TAG, "Export request is cancelled before handling the request");
125                 return;
126             }
127             final Uri uri = request.destUri;
128             final OutputStream outputStream;
129             try {
130                 outputStream = mResolver.openOutputStream(uri);
131             } catch (FileNotFoundException e) {
132                 Log.w(LOG_TAG, "FileNotFoundException thrown", e);
133                 // Need concise title.
134 
135                 final String errorReason =
136                     mService.getString(R.string.fail_reason_could_not_open_file,
137                             uri, e.getMessage());
138                 doFinishNotification(errorReason, null);
139                 return;
140             }
141 
142             final String exportType = request.exportType;
143             final int vcardType;
144             if (TextUtils.isEmpty(exportType)) {
145                 vcardType = VCardConfig.getVCardTypeFromString(
146                         mService.getString(R.string.config_export_vcard_type));
147             } else {
148                 vcardType = VCardConfig.getVCardTypeFromString(exportType);
149             }
150 
151             composer = new VCardComposer(mService, vcardType, true);
152 
153             // for test
154             // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
155             //     VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
156             // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
157 
158             writer = new BufferedWriter(new OutputStreamWriter(outputStream));
159             final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI;
160             // TODO: should provide better selection.
161             if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID},
162                     null, null,
163                     null, contentUriForRawContactsEntity)) {
164                 final String errorReason = composer.getErrorReason();
165                 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
166                 final String translatedErrorReason =
167                         translateComposerError(errorReason);
168                 final String title =
169                         mService.getString(R.string.fail_reason_could_not_initialize_exporter,
170                                 translatedErrorReason);
171                 doFinishNotification(title, null);
172                 return;
173             }
174 
175             final int total = composer.getCount();
176             if (total == 0) {
177                 final String title =
178                         mService.getString(R.string.fail_reason_no_exportable_contact);
179                 doFinishNotification(title, null);
180                 return;
181             }
182 
183             int current = 1;  // 1-origin
184             while (!composer.isAfterLast()) {
185                 if (isCancelled()) {
186                     Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
187                     return;
188                 }
189                 try {
190                     writer.write(composer.createOneEntry());
191                 } catch (IOException e) {
192                     final String errorReason = composer.getErrorReason();
193                     Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
194                     final String translatedErrorReason =
195                             translateComposerError(errorReason);
196                     final String title =
197                             mService.getString(R.string.fail_reason_error_occurred_during_export,
198                                     translatedErrorReason);
199                     doFinishNotification(title, null);
200                     return;
201                 }
202 
203                 // vCard export is quite fast (compared to import), and frequent notifications
204                 // bother notification bar too much.
205                 if (current % 100 == 1) {
206                     doProgressNotification(uri, total, current);
207                 }
208                 current++;
209             }
210             Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
211 
212             if (DEBUG) {
213                 Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath());
214             }
215             mService.updateMediaScanner(request.destUri.getPath());
216 
217             successful = true;
218             final String filename = request.displayName;
219             // If it is a local file (i.e. not a file from Drive), we need to allow user to share
220             // the file by pressing the notification; otherwise, it would be a file in Drive, we
221             // don't need to enable this action in notification since the file is already uploaded.
222             if (isLocalFile(uri)) {
223                 final Message msg = handler.obtainMessage();
224                 msg.arg1 = SHOW_READY_TOAST;
225                 handler.sendMessage(msg);
226                 doFinishNotificationWithShareAction(
227                         mService.getString(R.string.exporting_vcard_finished_title_fallback),
228                         mService.getString(R.string.touch_to_share_contacts), uri);
229             } else {
230                 final String title = filename == null
231                         ? mService.getString(R.string.exporting_vcard_finished_title_fallback)
232                         : mService.getString(R.string.exporting_vcard_finished_title, filename);
233                 doFinishNotification(title, null);
234             }
235         } finally {
236             if (composer != null) {
237                 composer.terminate();
238             }
239             if (writer != null) {
240                 try {
241                     writer.close();
242                 } catch (IOException e) {
243                     Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e);
244                 }
245             }
246             mService.handleFinishExportNotification(mJobId, successful);
247         }
248     }
249 
isLocalFile(Uri uri)250     private boolean isLocalFile(Uri uri) {
251         final String authority = uri.getAuthority();
252         return mService.getString(R.string.contacts_file_provider_authority).equals(authority);
253     }
254 
translateComposerError(String errorMessage)255     private String translateComposerError(String errorMessage) {
256         final Resources resources = mService.getResources();
257         if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
258             return resources.getString(R.string.composer_failed_to_get_database_infomation);
259         } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
260             return resources.getString(R.string.composer_has_no_exportable_contact);
261         } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
262             return resources.getString(R.string.composer_not_initialized);
263         } else {
264             return errorMessage;
265         }
266     }
267 
doProgressNotification(Uri uri, int totalCount, int currentCount)268     private void doProgressNotification(Uri uri, int totalCount, int currentCount) {
269         final String displayName = uri.getLastPathSegment();
270         final String description =
271                 mService.getString(R.string.exporting_contact_list_message, displayName);
272         final String tickerText =
273                 mService.getString(R.string.exporting_contact_list_title);
274         final Notification notification =
275                 NotificationImportExportListener.constructProgressNotification(mService,
276                         VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
277                         totalCount, currentCount);
278         mService.startForeground(mJobId, notification);
279     }
280 
doCancelNotification()281     private void doCancelNotification() {
282         if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
283         final String description = mService.getString(R.string.exporting_vcard_canceled_title,
284                 mExportRequest.destUri.getLastPathSegment());
285         final Notification notification =
286                 NotificationImportExportListener.constructCancelNotification(mService, description);
287         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
288                 mJobId, notification);
289     }
290 
doFinishNotification(final String title, final String description)291     private void doFinishNotification(final String title, final String description) {
292         if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
293         final Intent intent = new Intent();
294         intent.setClassName(mService, mCallingActivity);
295         final Notification notification =
296                 NotificationImportExportListener.constructFinishNotification(mService, title,
297                         description, intent);
298         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
299                 mJobId, notification);
300     }
301 
302     /**
303      * Pass intent with ACTION_SEND to notification so that user can press the notification to
304      * share contacts.
305      */
doFinishNotificationWithShareAction(final String title, final String description, Uri uri)306     private void doFinishNotificationWithShareAction(final String title, final String
307             description, Uri uri) {
308         if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
309         final Intent intent = new Intent(Intent.ACTION_SEND);
310         intent.setType(Contacts.CONTENT_VCARD_TYPE);
311         intent.putExtra(Intent.EXTRA_STREAM, uri);
312         // Securely grant access using temporary access permissions
313         // Use FLAG_ACTIVITY_NEW_TASK to set it as new task, to get rid of cached files.
314         intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
315         // Build notification
316         final Notification notification =
317                 NotificationImportExportListener.constructFinishNotification(
318                         mService, title, description, intent);
319         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
320                 mJobId, notification);
321     }
322 
323     @Override
cancel(boolean mayInterruptIfRunning)324     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
325         if (DEBUG) Log.d(LOG_TAG, "received cancel request");
326         if (mDone || mCanceled) {
327             return false;
328         }
329         mCanceled = true;
330         return true;
331     }
332 
333     @Override
isCancelled()334     public synchronized boolean isCancelled() {
335         return mCanceled;
336     }
337 
338     @Override
isDone()339     public synchronized boolean isDone() {
340         return mDone;
341     }
342 
getRequest()343     public ExportRequest getRequest() {
344         return mExportRequest;
345     }
346 }
347