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