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