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     }
85 
86     @Override
getType()87     public final int getType() {
88         return VCardService.TYPE_EXPORT;
89     }
90 
91     @Override
run()92     public void run() {
93         // ExecutorService ignores RuntimeException, so we need to show it here.
94         try {
95             runInternal();
96 
97             if (isCancelled()) {
98                 doCancelNotification();
99             }
100         } catch (OutOfMemoryError|RuntimeException e) {
101             FeedbackHelper.sendFeedback(mService, LOG_TAG, "Failed to process vcard export", e);
102             throw e;
103         } finally {
104             synchronized (this) {
105                 mDone = true;
106             }
107         }
108     }
109 
runInternal()110     private void runInternal() {
111         if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
112         final ExportRequest request = mExportRequest;
113         VCardComposer composer = null;
114         Writer writer = null;
115         boolean successful = false;
116         try {
117             if (isCancelled()) {
118                 Log.i(LOG_TAG, "Export request is cancelled before handling the request");
119                 return;
120             }
121             final Uri uri = request.destUri;
122             final OutputStream outputStream;
123             try {
124                 outputStream = mResolver.openOutputStream(uri);
125             } catch (FileNotFoundException e) {
126                 Log.w(LOG_TAG, "FileNotFoundException thrown", e);
127                 // Need concise title.
128 
129                 final String errorReason =
130                     mService.getString(R.string.fail_reason_could_not_open_file,
131                             uri, e.getMessage());
132                 doFinishNotification(errorReason, null);
133                 return;
134             }
135 
136             final String exportType = request.exportType;
137             final int vcardType;
138             if (TextUtils.isEmpty(exportType)) {
139                 vcardType = VCardConfig.getVCardTypeFromString(
140                         mService.getString(R.string.config_export_vcard_type));
141             } else {
142                 vcardType = VCardConfig.getVCardTypeFromString(exportType);
143             }
144 
145             composer = new VCardComposer(mService, vcardType, true);
146 
147             // for test
148             // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
149             //     VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
150             // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
151 
152             writer = new BufferedWriter(new OutputStreamWriter(outputStream));
153             final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI;
154             // TODO: should provide better selection.
155             if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID},
156                     null, null,
157                     null, contentUriForRawContactsEntity)) {
158                 final String errorReason = composer.getErrorReason();
159                 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
160                 final String translatedErrorReason =
161                         translateComposerError(errorReason);
162                 final String title =
163                         mService.getString(R.string.fail_reason_could_not_initialize_exporter,
164                                 translatedErrorReason);
165                 doFinishNotification(title, null);
166                 return;
167             }
168 
169             final int total = composer.getCount();
170             if (total == 0) {
171                 final String title =
172                         mService.getString(R.string.fail_reason_no_exportable_contact);
173                 doFinishNotification(title, null);
174                 return;
175             }
176 
177             int current = 1;  // 1-origin
178             while (!composer.isAfterLast()) {
179                 if (isCancelled()) {
180                     Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
181                     return;
182                 }
183                 try {
184                     writer.write(composer.createOneEntry());
185                 } catch (IOException e) {
186                     final String errorReason = composer.getErrorReason();
187                     Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
188                     final String translatedErrorReason =
189                             translateComposerError(errorReason);
190                     final String title =
191                             mService.getString(R.string.fail_reason_error_occurred_during_export,
192                                     translatedErrorReason);
193                     doFinishNotification(title, null);
194                     return;
195                 }
196 
197                 // vCard export is quite fast (compared to import), and frequent notifications
198                 // bother notification bar too much.
199                 if (current % 100 == 1) {
200                     doProgressNotification(uri, total, current);
201                 }
202                 current++;
203             }
204             Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
205 
206             if (DEBUG) {
207                 Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath());
208             }
209             mService.updateMediaScanner(request.destUri.getPath());
210 
211             successful = true;
212             final String filename = ExportVCardActivity.getOpenableUriDisplayName(mService, uri);
213             // If it is a local file (i.e. not a file from Drive), we need to allow user to share
214             // the file by pressing the notification; otherwise, it would be a file in Drive, we
215             // don't need to enable this action in notification since the file is already uploaded.
216             if (isLocalFile(uri)) {
217                 final Message msg = handler.obtainMessage();
218                 msg.arg1 = SHOW_READY_TOAST;
219                 handler.sendMessage(msg);
220                 doFinishNotificationWithShareAction(
221                         mService.getString(R.string.exporting_vcard_finished_title_fallback),
222                         mService.getString(R.string.touch_to_share_contacts), uri);
223             } else {
224                 final String title = filename == null
225                         ? mService.getString(R.string.exporting_vcard_finished_title_fallback)
226                         : mService.getString(R.string.exporting_vcard_finished_title, filename);
227                 doFinishNotification(title, null);
228             }
229         } finally {
230             if (composer != null) {
231                 composer.terminate();
232             }
233             if (writer != null) {
234                 try {
235                     writer.close();
236                 } catch (IOException e) {
237                     Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e);
238                 }
239             }
240             mService.handleFinishExportNotification(mJobId, successful);
241         }
242     }
243 
isLocalFile(Uri uri)244     private boolean isLocalFile(Uri uri) {
245         final String authority = uri.getAuthority();
246         return mService.getString(R.string.contacts_file_provider_authority).equals(authority);
247     }
248 
translateComposerError(String errorMessage)249     private String translateComposerError(String errorMessage) {
250         final Resources resources = mService.getResources();
251         if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
252             return resources.getString(R.string.composer_failed_to_get_database_infomation);
253         } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
254             return resources.getString(R.string.composer_has_no_exportable_contact);
255         } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
256             return resources.getString(R.string.composer_not_initialized);
257         } else {
258             return errorMessage;
259         }
260     }
261 
doProgressNotification(Uri uri, int totalCount, int currentCount)262     private void doProgressNotification(Uri uri, int totalCount, int currentCount) {
263         final String displayName = uri.getLastPathSegment();
264         final String description =
265                 mService.getString(R.string.exporting_contact_list_message, displayName);
266         final String tickerText =
267                 mService.getString(R.string.exporting_contact_list_title);
268         final Notification notification =
269                 NotificationImportExportListener.constructProgressNotification(mService,
270                         VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
271                         totalCount, currentCount);
272         mService.startForeground(mJobId, notification);
273     }
274 
doCancelNotification()275     private void doCancelNotification() {
276         if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
277         final String description = mService.getString(R.string.exporting_vcard_canceled_title,
278                 mExportRequest.destUri.getLastPathSegment());
279         final Notification notification =
280                 NotificationImportExportListener.constructCancelNotification(mService, description);
281         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
282                 mJobId, notification);
283     }
284 
doFinishNotification(final String title, final String description)285     private void doFinishNotification(final String title, final String description) {
286         if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
287         final Intent intent = new Intent();
288         intent.setClassName(mService, mCallingActivity);
289         final Notification notification =
290                 NotificationImportExportListener.constructFinishNotification(mService, title,
291                         description, intent);
292         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
293                 mJobId, notification);
294     }
295 
296     /**
297      * Pass intent with ACTION_SEND to notification so that user can press the notification to
298      * share contacts.
299      */
doFinishNotificationWithShareAction(final String title, final String description, Uri uri)300     private void doFinishNotificationWithShareAction(final String title, final String
301             description, Uri uri) {
302         if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
303         final Intent intent = new Intent(Intent.ACTION_SEND);
304         intent.setType(Contacts.CONTENT_VCARD_TYPE);
305         intent.putExtra(Intent.EXTRA_STREAM, uri);
306         // Securely grant access using temporary access permissions
307         intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
308         // Build notification
309         final Notification notification =
310                 NotificationImportExportListener.constructFinishNotificationWithFlags(
311                         mService, title, description, intent, Intent.FLAG_ACTIVITY_NEW_TASK);
312         mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
313                 mJobId, notification);
314     }
315 
316     @Override
cancel(boolean mayInterruptIfRunning)317     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
318         if (DEBUG) Log.d(LOG_TAG, "received cancel request");
319         if (mDone || mCanceled) {
320             return false;
321         }
322         mCanceled = true;
323         return true;
324     }
325 
326     @Override
isCancelled()327     public synchronized boolean isCancelled() {
328         return mCanceled;
329     }
330 
331     @Override
isDone()332     public synchronized boolean isDone() {
333         return mDone;
334     }
335 
getRequest()336     public ExportRequest getRequest() {
337         return mExportRequest;
338     }
339 }
340