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