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; 142 // TODO: should provide better selection. 143 if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID}, 144 null, null, 145 null, contentUriForRawContactsEntity)) { 146 final String errorReason = composer.getErrorReason(); 147 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason); 148 final String translatedErrorReason = 149 translateComposerError(errorReason); 150 final String title = 151 mService.getString(R.string.fail_reason_could_not_initialize_exporter, 152 translatedErrorReason); 153 doFinishNotification(title, null); 154 return; 155 } 156 157 final int total = composer.getCount(); 158 if (total == 0) { 159 final String title = 160 mService.getString(R.string.fail_reason_no_exportable_contact); 161 doFinishNotification(title, null); 162 return; 163 } 164 165 int current = 1; // 1-origin 166 while (!composer.isAfterLast()) { 167 if (isCancelled()) { 168 Log.i(LOG_TAG, "Export request is cancelled during composing vCard"); 169 return; 170 } 171 try { 172 writer.write(composer.createOneEntry()); 173 } catch (IOException e) { 174 final String errorReason = composer.getErrorReason(); 175 Log.e(LOG_TAG, "Failed to read a contact: " + errorReason); 176 final String translatedErrorReason = 177 translateComposerError(errorReason); 178 final String title = 179 mService.getString(R.string.fail_reason_error_occurred_during_export, 180 translatedErrorReason); 181 doFinishNotification(title, null); 182 return; 183 } 184 185 // vCard export is quite fast (compared to import), and frequent notifications 186 // bother notification bar too much. 187 if (current % 100 == 1) { 188 doProgressNotification(uri, total, current); 189 } 190 current++; 191 } 192 Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri); 193 194 if (DEBUG) { 195 Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath()); 196 } 197 mService.updateMediaScanner(request.destUri.getPath()); 198 199 successful = true; 200 final String filename = uri.getLastPathSegment(); 201 final String title = mService.getString(R.string.exporting_vcard_finished_title, 202 filename); 203 doFinishNotification(title, null); 204 } finally { 205 if (composer != null) { 206 composer.terminate(); 207 } 208 if (writer != null) { 209 try { 210 writer.close(); 211 } catch (IOException e) { 212 Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e); 213 } 214 } 215 mService.handleFinishExportNotification(mJobId, successful); 216 } 217 } 218 219 private String translateComposerError(String errorMessage) { 220 final Resources resources = mService.getResources(); 221 if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) { 222 return resources.getString(R.string.composer_failed_to_get_database_infomation); 223 } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) { 224 return resources.getString(R.string.composer_has_no_exportable_contact); 225 } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) { 226 return resources.getString(R.string.composer_not_initialized); 227 } else { 228 return errorMessage; 229 } 230 } 231 232 private void doProgressNotification(Uri uri, int totalCount, int currentCount) { 233 final String displayName = uri.getLastPathSegment(); 234 final String description = 235 mService.getString(R.string.exporting_contact_list_message, displayName); 236 final String tickerText = 237 mService.getString(R.string.exporting_contact_list_title); 238 final Notification notification = 239 NotificationImportExportListener.constructProgressNotification(mService, 240 VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName, 241 totalCount, currentCount); 242 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, 243 mJobId, notification); 244 } 245 246 private void doCancelNotification() { 247 if (DEBUG) Log.d(LOG_TAG, "send cancel notification"); 248 final String description = mService.getString(R.string.exporting_vcard_canceled_title, 249 mExportRequest.destUri.getLastPathSegment()); 250 final Notification notification = 251 NotificationImportExportListener.constructCancelNotification(mService, description); 252 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, 253 mJobId, notification); 254 } 255 256 private void doFinishNotification(final String title, final String description) { 257 if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description); 258 final Intent intent = new Intent(); 259 intent.setClassName(mService, mCallingActivity); 260 final Notification notification = 261 NotificationImportExportListener.constructFinishNotification(mService, title, 262 description, intent); 263 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, 264 mJobId, notification); 265 } 266 267 @Override 268 public synchronized boolean cancel(boolean mayInterruptIfRunning) { 269 if (DEBUG) Log.d(LOG_TAG, "received cancel request"); 270 if (mDone || mCanceled) { 271 return false; 272 } 273 mCanceled = true; 274 return true; 275 } 276 277 @Override 278 public synchronized boolean isCancelled() { 279 return mCanceled; 280 } 281 282 @Override 283 public synchronized boolean isDone() { 284 return mDone; 285 } 286 287 public ExportRequest getRequest() { 288 return mExportRequest; 289 } 290 } 291