Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 | */ |
Gary Mai | 69c182a | 2016-12-05 13:07:03 -0800 | [diff] [blame] | 16 | package com.android.contacts.vcard; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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; |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 25 | import android.os.Handler; |
| 26 | import android.os.Message; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 27 | import android.provider.ContactsContract.Contacts; |
| 28 | import android.provider.ContactsContract.RawContactsEntity; |
| 29 | import android.text.TextUtils; |
| 30 | import android.util.Log; |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 31 | import android.widget.Toast; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 32 | |
Arthur Wang | 3f6a244 | 2016-12-05 14:51:59 -0800 | [diff] [blame] | 33 | import com.android.contacts.R; |
Walter Jang | 3a0b483 | 2016-10-12 11:02:54 -0700 | [diff] [blame] | 34 | import com.android.contactsbind.FeedbackHelper; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 60 | private volatile boolean mCanceled; |
| 61 | private volatile boolean mDone; |
| 62 | |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 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 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 |
| 87 | public final int getType() { |
| 88 | return VCardService.TYPE_EXPORT; |
| 89 | } |
| 90 | |
| 91 | @Override |
| 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 | } |
Walter Jang | 3a0b483 | 2016-10-12 11:02:54 -0700 | [diff] [blame] | 100 | } catch (OutOfMemoryError|RuntimeException e) { |
| 101 | FeedbackHelper.sendFeedback(mService, LOG_TAG, "Failed to process vcard export", e); |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 102 | throw e; |
| 103 | } finally { |
| 104 | synchronized (this) { |
| 105 | mDone = true; |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 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)); |
Yorke Lee | d4793dc | 2013-12-03 13:15:31 -0800 | [diff] [blame] | 153 | final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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; |
Walter Jang | 7a24301 | 2015-07-09 10:19:35 -0700 | [diff] [blame] | 212 | final String filename = ExportVCardActivity.getOpenableUriDisplayName(mService, uri); |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 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 | } |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 | |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 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 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 | |
| 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 | mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, |
| 273 | mJobId, notification); |
| 274 | } |
| 275 | |
| 276 | private void doCancelNotification() { |
| 277 | if (DEBUG) Log.d(LOG_TAG, "send cancel notification"); |
| 278 | final String description = mService.getString(R.string.exporting_vcard_canceled_title, |
| 279 | mExportRequest.destUri.getLastPathSegment()); |
| 280 | final Notification notification = |
| 281 | NotificationImportExportListener.constructCancelNotification(mService, description); |
| 282 | mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, |
| 283 | mJobId, notification); |
| 284 | } |
| 285 | |
| 286 | private void doFinishNotification(final String title, final String description) { |
| 287 | if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description); |
| 288 | final Intent intent = new Intent(); |
| 289 | intent.setClassName(mService, mCallingActivity); |
| 290 | final Notification notification = |
| 291 | NotificationImportExportListener.constructFinishNotification(mService, title, |
| 292 | description, intent); |
| 293 | mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, |
| 294 | mJobId, notification); |
| 295 | } |
| 296 | |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 297 | /** |
| 298 | * Pass intent with ACTION_SEND to notification so that user can press the notification to |
| 299 | * share contacts. |
| 300 | */ |
| 301 | private void doFinishNotificationWithShareAction(final String title, final String |
| 302 | description, Uri uri) { |
| 303 | if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description); |
| 304 | final Intent intent = new Intent(Intent.ACTION_SEND); |
| 305 | intent.setType(Contacts.CONTENT_VCARD_TYPE); |
| 306 | intent.putExtra(Intent.EXTRA_STREAM, uri); |
Wenyi Wang | b81f6eb | 2016-03-02 11:35:36 -0800 | [diff] [blame] | 307 | // Securely grant access using temporary access permissions |
| 308 | intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| 309 | // Build notification |
Wenyi Wang | 142a344 | 2016-02-04 14:08:45 -0800 | [diff] [blame] | 310 | final Notification notification = |
| 311 | NotificationImportExportListener.constructFinishNotificationWithFlags( |
| 312 | mService, title, description, intent, Intent.FLAG_ACTIVITY_NEW_TASK); |
| 313 | mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, |
| 314 | mJobId, notification); |
| 315 | } |
| 316 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 317 | @Override |
| 318 | public synchronized boolean cancel(boolean mayInterruptIfRunning) { |
| 319 | if (DEBUG) Log.d(LOG_TAG, "received cancel request"); |
| 320 | if (mDone || mCanceled) { |
| 321 | return false; |
| 322 | } |
| 323 | mCanceled = true; |
| 324 | return true; |
| 325 | } |
| 326 | |
| 327 | @Override |
| 328 | public synchronized boolean isCancelled() { |
| 329 | return mCanceled; |
| 330 | } |
| 331 | |
| 332 | @Override |
| 333 | public synchronized boolean isDone() { |
| 334 | return mDone; |
| 335 | } |
| 336 | |
| 337 | public ExportRequest getRequest() { |
| 338 | return mExportRequest; |
| 339 | } |
| 340 | } |