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