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 */
16package com.android.contacts.vcard;
17
18import com.android.contacts.R;
19import com.android.contacts.activities.PeopleActivity;
20import com.android.vcard.VCardComposer;
21import com.android.vcard.VCardConfig;
22
23import android.app.Notification;
24import android.app.NotificationManager;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.res.Resources;
29import android.net.Uri;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.RawContactsEntity;
32import android.text.TextUtils;
33import android.util.Log;
34
35import java.io.BufferedWriter;
36import java.io.FileNotFoundException;
37import java.io.IOException;
38import java.io.OutputStream;
39import java.io.OutputStreamWriter;
40import java.io.Writer;
41
42/**
43 * Class for processing one export request from a user. Dropped after exporting requested Uri(s).
44 * {@link VCardService} will create another object when there is another export request.
45 */
46public class ExportProcessor extends ProcessorBase {
47    private static final String LOG_TAG = "VCardExport";
48    private static final boolean DEBUG = VCardService.DEBUG;
49
50    private final VCardService mService;
51    private final ContentResolver mResolver;
52    private final NotificationManager mNotificationManager;
53    private final ExportRequest mExportRequest;
54    private final int mJobId;
55
56    private volatile boolean mCanceled;
57    private volatile boolean mDone;
58
59    public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId) {
60        mService = service;
61        mResolver = service.getContentResolver();
62        mNotificationManager =
63                (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
64        mExportRequest = exportRequest;
65        mJobId = jobId;
66    }
67
68    @Override
69    public final int getType() {
70        return VCardService.TYPE_EXPORT;
71    }
72
73    @Override
74    public void run() {
75        // ExecutorService ignores RuntimeException, so we need to show it here.
76        try {
77            runInternal();
78
79            if (isCancelled()) {
80                doCancelNotification();
81            }
82        } catch (OutOfMemoryError e) {
83            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
84            throw e;
85        } catch (RuntimeException e) {
86            Log.e(LOG_TAG, "RuntimeException thrown during export", e);
87            throw e;
88        } finally {
89            synchronized (this) {
90                mDone = true;
91            }
92        }
93    }
94
95    private void runInternal() {
96        if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
97        final ExportRequest request = mExportRequest;
98        VCardComposer composer = null;
99        Writer writer = null;
100        boolean successful = false;
101        try {
102            if (isCancelled()) {
103                Log.i(LOG_TAG, "Export request is cancelled before handling the request");
104                return;
105            }
106            final Uri uri = request.destUri;
107            final OutputStream outputStream;
108            try {
109                outputStream = mResolver.openOutputStream(uri);
110            } catch (FileNotFoundException e) {
111                Log.w(LOG_TAG, "FileNotFoundException thrown", e);
112                // Need concise title.
113
114                final String errorReason =
115                    mService.getString(R.string.fail_reason_could_not_open_file,
116                            uri, e.getMessage());
117                doFinishNotification(errorReason, null);
118                return;
119            }
120
121            final String exportType = request.exportType;
122            final int vcardType;
123            if (TextUtils.isEmpty(exportType)) {
124                vcardType = VCardConfig.getVCardTypeFromString(
125                        mService.getString(R.string.config_export_vcard_type));
126            } else {
127                vcardType = VCardConfig.getVCardTypeFromString(exportType);
128            }
129
130            composer = new VCardComposer(mService, vcardType, true);
131
132            // for test
133            // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
134            //     VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
135            // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
136
137            writer = new BufferedWriter(new OutputStreamWriter(outputStream));
138            final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI.buildUpon()
139                    .appendQueryParameter(RawContactsEntity.FOR_EXPORT_ONLY, "1")
140                    .build();
141            // TODO: should provide better selection.
142            if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID},
143                    null, null,
144                    null, contentUriForRawContactsEntity)) {
145                final String errorReason = composer.getErrorReason();
146                Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
147                final String translatedErrorReason =
148                        translateComposerError(errorReason);
149                final String title =
150                        mService.getString(R.string.fail_reason_could_not_initialize_exporter,
151                                translatedErrorReason);
152                doFinishNotification(title, null);
153                return;
154            }
155
156            final int total = composer.getCount();
157            if (total == 0) {
158                final String title =
159                        mService.getString(R.string.fail_reason_no_exportable_contact);
160                doFinishNotification(title, null);
161                return;
162            }
163
164            int current = 1;  // 1-origin
165            while (!composer.isAfterLast()) {
166                if (isCancelled()) {
167                    Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
168                    return;
169                }
170                try {
171                    writer.write(composer.createOneEntry());
172                } catch (IOException e) {
173                    final String errorReason = composer.getErrorReason();
174                    Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
175                    final String translatedErrorReason =
176                            translateComposerError(errorReason);
177                    final String title =
178                            mService.getString(R.string.fail_reason_error_occurred_during_export,
179                                    translatedErrorReason);
180                    doFinishNotification(title, null);
181                    return;
182                }
183
184                // vCard export is quite fast (compared to import), and frequent notifications
185                // bother notification bar too much.
186                if (current % 100 == 1) {
187                    doProgressNotification(uri, total, current);
188                }
189                current++;
190            }
191            Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
192
193            if (DEBUG) {
194                Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath());
195            }
196            mService.updateMediaScanner(request.destUri.getPath());
197
198            successful = true;
199            final String filename = uri.getLastPathSegment();
200            final String title = mService.getString(R.string.exporting_vcard_finished_title,
201                    filename);
202            doFinishNotification(title, null);
203        } finally {
204            if (composer != null) {
205                composer.terminate();
206            }
207            if (writer != null) {
208                try {
209                    writer.close();
210                } catch (IOException e) {
211                    Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e);
212                }
213            }
214            mService.handleFinishExportNotification(mJobId, successful);
215        }
216    }
217
218    private String translateComposerError(String errorMessage) {
219        final Resources resources = mService.getResources();
220        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
221            return resources.getString(R.string.composer_failed_to_get_database_infomation);
222        } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
223            return resources.getString(R.string.composer_has_no_exportable_contact);
224        } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
225            return resources.getString(R.string.composer_not_initialized);
226        } else {
227            return errorMessage;
228        }
229    }
230
231    private void doProgressNotification(Uri uri, int totalCount, int currentCount) {
232        final String displayName = uri.getLastPathSegment();
233        final String description =
234                mService.getString(R.string.exporting_contact_list_message, displayName);
235        final String tickerText =
236                mService.getString(R.string.exporting_contact_list_title);
237        final Notification notification =
238                NotificationImportExportListener.constructProgressNotification(mService,
239                        VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
240                        totalCount, currentCount);
241        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
242                mJobId, notification);
243    }
244
245    private void doCancelNotification() {
246        if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
247        final String description = mService.getString(R.string.exporting_vcard_canceled_title,
248                mExportRequest.destUri.getLastPathSegment());
249        final Notification notification =
250                NotificationImportExportListener.constructCancelNotification(mService, description);
251        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
252                mJobId, notification);
253    }
254
255    private void doFinishNotification(final String title, final String description) {
256        if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
257        final Intent intent = new Intent(mService, PeopleActivity.class);
258        final Notification notification =
259                NotificationImportExportListener.constructFinishNotification(mService, title,
260                        description, intent);
261        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
262                mJobId, notification);
263    }
264
265    @Override
266    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
267        if (DEBUG) Log.d(LOG_TAG, "received cancel request");
268        if (mDone || mCanceled) {
269            return false;
270        }
271        mCanceled = true;
272        return true;
273    }
274
275    @Override
276    public synchronized boolean isCancelled() {
277        return mCanceled;
278    }
279
280    @Override
281    public synchronized boolean isDone() {
282        return mDone;
283    }
284
285    public ExportRequest getRequest() {
286        return mExportRequest;
287    }
288}
289