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.common.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.provider.ContactsContract.Contacts;
     26 import android.provider.ContactsContract.RawContactsEntity;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 
     30 import com.android.contacts.common.R;
     31 import com.android.vcard.VCardComposer;
     32 import com.android.vcard.VCardConfig;
     33 
     34 import java.io.BufferedWriter;
     35 import java.io.FileNotFoundException;
     36 import java.io.IOException;
     37 import java.io.OutputStream;
     38 import java.io.OutputStreamWriter;
     39 import 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  */
     45 public 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