blob: 48420da83735f1660194269d2ad2ee6e915dd62d [file] [log] [blame]
Chiao Chengd80c4342012-12-03 17:15:58 -08001/*
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 */
16package com.android.contacts.common.vcard;
17
18import android.app.Notification;
19import android.app.NotificationManager;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.net.Uri;
Wenyi Wang142a3442016-02-04 14:08:45 -080025import android.os.Handler;
26import android.os.Message;
Chiao Chengd80c4342012-12-03 17:15:58 -080027import android.provider.ContactsContract.Contacts;
28import android.provider.ContactsContract.RawContactsEntity;
29import android.text.TextUtils;
30import android.util.Log;
Wenyi Wang142a3442016-02-04 14:08:45 -080031import android.widget.Toast;
Chiao Chengd80c4342012-12-03 17:15:58 -080032
33import com.android.contacts.common.R;
Walter Jang3a0b4832016-10-12 11:02:54 -070034import com.android.contactsbind.FeedbackHelper;
Chiao Chengd80c4342012-12-03 17:15:58 -080035import com.android.vcard.VCardComposer;
36import com.android.vcard.VCardConfig;
37
38import java.io.BufferedWriter;
39import java.io.FileNotFoundException;
40import java.io.IOException;
41import java.io.OutputStream;
42import java.io.OutputStreamWriter;
43import 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 */
49public 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
Chiao Chengd80c4342012-12-03 17:15:58 -080060 private volatile boolean mCanceled;
61 private volatile boolean mDone;
62
Wenyi Wang142a3442016-02-04 14:08:45 -080063 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
Chiao Chengd80c4342012-12-03 17:15:58 -080075 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
87 public final int getType() {
88 return VCardService.TYPE_EXPORT;
89 }
90
91 @Override
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 }
Walter Jang3a0b4832016-10-12 11:02:54 -0700100 } catch (OutOfMemoryError|RuntimeException e) {
101 FeedbackHelper.sendFeedback(mService, LOG_TAG, "Failed to process vcard export", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800102 throw e;
103 } finally {
104 synchronized (this) {
105 mDone = true;
106 }
107 }
108 }
109
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));
Yorke Leed4793dc2013-12-03 13:15:31 -0800153 final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI;
Chiao Chengd80c4342012-12-03 17:15:58 -0800154 // 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;
Walter Jang7a243012015-07-09 10:19:35 -0700212 final String filename = ExportVCardActivity.getOpenableUriDisplayName(mService, uri);
Wenyi Wang142a3442016-02-04 14:08:45 -0800213 // 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 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800229 } 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
Wenyi Wang142a3442016-02-04 14:08:45 -0800244 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
Chiao Chengd80c4342012-12-03 17:15:58 -0800249 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
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 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
273 mJobId, notification);
274 }
275
276 private void doCancelNotification() {
277 if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
278 final String description = mService.getString(R.string.exporting_vcard_canceled_title,
279 mExportRequest.destUri.getLastPathSegment());
280 final Notification notification =
281 NotificationImportExportListener.constructCancelNotification(mService, description);
282 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
283 mJobId, notification);
284 }
285
286 private void doFinishNotification(final String title, final String description) {
287 if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
288 final Intent intent = new Intent();
289 intent.setClassName(mService, mCallingActivity);
290 final Notification notification =
291 NotificationImportExportListener.constructFinishNotification(mService, title,
292 description, intent);
293 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
294 mJobId, notification);
295 }
296
Wenyi Wang142a3442016-02-04 14:08:45 -0800297 /**
298 * Pass intent with ACTION_SEND to notification so that user can press the notification to
299 * share contacts.
300 */
301 private void doFinishNotificationWithShareAction(final String title, final String
302 description, Uri uri) {
303 if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
304 final Intent intent = new Intent(Intent.ACTION_SEND);
305 intent.setType(Contacts.CONTENT_VCARD_TYPE);
306 intent.putExtra(Intent.EXTRA_STREAM, uri);
Wenyi Wangb81f6eb2016-03-02 11:35:36 -0800307 // Securely grant access using temporary access permissions
308 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
309 // Build notification
Wenyi Wang142a3442016-02-04 14:08:45 -0800310 final Notification notification =
311 NotificationImportExportListener.constructFinishNotificationWithFlags(
312 mService, title, description, intent, Intent.FLAG_ACTIVITY_NEW_TASK);
313 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
314 mJobId, notification);
315 }
316
Chiao Chengd80c4342012-12-03 17:15:58 -0800317 @Override
318 public synchronized boolean cancel(boolean mayInterruptIfRunning) {
319 if (DEBUG) Log.d(LOG_TAG, "received cancel request");
320 if (mDone || mCanceled) {
321 return false;
322 }
323 mCanceled = true;
324 return true;
325 }
326
327 @Override
328 public synchronized boolean isCancelled() {
329 return mCanceled;
330 }
331
332 @Override
333 public synchronized boolean isDone() {
334 return mDone;
335 }
336
337 public ExportRequest getRequest() {
338 return mExportRequest;
339 }
340}