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