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