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