Home | History | Annotate | Download | only in vcard
      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 com.android.contacts.R;
     19 import com.android.contacts.activities.PeopleActivity;
     20 import com.android.vcard.VCardComposer;
     21 import com.android.vcard.VCardConfig;
     22 
     23 import android.app.Notification;
     24 import android.app.NotificationManager;
     25 import android.content.ContentResolver;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.res.Resources;
     29 import android.net.Uri;
     30 import android.provider.ContactsContract.Contacts;
     31 import android.provider.ContactsContract.RawContactsEntity;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 
     35 import java.io.BufferedWriter;
     36 import java.io.FileNotFoundException;
     37 import java.io.IOException;
     38 import java.io.OutputStream;
     39 import java.io.OutputStreamWriter;
     40 import 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  */
     46 public 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