1 /* 2 * Copyright (C) 2009 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; 17 18 import android.app.Activity; 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.ProgressDialog; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.res.Resources; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.PowerManager; 28 import android.pim.vcard.VCardComposer; 29 import android.pim.vcard.VCardConfig; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import java.io.File; 34 import java.io.FileNotFoundException; 35 import java.io.FileOutputStream; 36 import java.io.OutputStream; 37 import java.util.HashSet; 38 import java.util.Set; 39 40 /** 41 * Class for exporting vCard. 42 * 43 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be 44 * finished (with the method {@link Activity#finish()}) after the export and never reuse 45 * any Dialog in the instance. So this code is careless about the management around managed 46 * dialogs stuffs (like how onCreateDialog() is used). 47 */ 48 public class ExportVCardActivity extends Activity { 49 private static final String LOG_TAG = "ExportVCardActivity"; 50 51 // If true, VCardExporter is able to emits files longer than 8.3 format. 52 private static final boolean ALLOW_LONG_FILE_NAME = false; 53 private String mTargetDirectory; 54 private String mFileNamePrefix; 55 private String mFileNameSuffix; 56 private int mFileIndexMinimum; 57 private int mFileIndexMaximum; 58 private String mFileNameExtension; 59 private String mVCardTypeStr; 60 private Set<String> mExtensionsToConsider; 61 62 private ProgressDialog mProgressDialog; 63 private String mExportingFileName; 64 65 private Handler mHandler = new Handler(); 66 67 // Used temporaly when asking users to confirm the file name 68 private String mTargetFileName; 69 70 // String for storing error reason temporaly. 71 private String mErrorReason; 72 73 private ActualExportThread mActualExportThread; 74 75 private class CancelListener 76 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { 77 public void onClick(DialogInterface dialog, int which) { 78 finish(); 79 } 80 public void onCancel(DialogInterface dialog) { 81 finish(); 82 } 83 } 84 85 private CancelListener mCancelListener = new CancelListener(); 86 87 private class ErrorReasonDisplayer implements Runnable { 88 private final int mResId; 89 public ErrorReasonDisplayer(int resId) { 90 mResId = resId; 91 } 92 public ErrorReasonDisplayer(String errorReason) { 93 mResId = R.id.dialog_fail_to_export_with_reason; 94 mErrorReason = errorReason; 95 } 96 public void run() { 97 // Show the Dialog only when the parent Activity is still alive. 98 if (!ExportVCardActivity.this.isFinishing()) { 99 showDialog(mResId); 100 } 101 } 102 } 103 104 private class ExportConfirmationListener implements DialogInterface.OnClickListener { 105 private final String mFileName; 106 107 public ExportConfirmationListener(String fileName) { 108 mFileName = fileName; 109 } 110 111 public void onClick(DialogInterface dialog, int which) { 112 if (which == DialogInterface.BUTTON_POSITIVE) { 113 mActualExportThread = new ActualExportThread(mFileName); 114 showDialog(R.id.dialog_exporting_vcard); 115 } 116 } 117 } 118 119 private class ActualExportThread extends Thread 120 implements DialogInterface.OnCancelListener { 121 private PowerManager.WakeLock mWakeLock; 122 private boolean mCanceled = false; 123 124 public ActualExportThread(String fileName) { 125 mExportingFileName = fileName; 126 PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE); 127 mWakeLock = powerManager.newWakeLock( 128 PowerManager.SCREEN_DIM_WAKE_LOCK | 129 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 130 } 131 132 @Override 133 public void run() { 134 boolean shouldCallFinish = true; 135 mWakeLock.acquire(); 136 VCardComposer composer = null; 137 try { 138 OutputStream outputStream = null; 139 try { 140 outputStream = new FileOutputStream(mExportingFileName); 141 } catch (FileNotFoundException e) { 142 final String errorReason = 143 getString(R.string.fail_reason_could_not_open_file, 144 mExportingFileName, e.getMessage()); 145 shouldCallFinish = false; 146 mHandler.post(new ErrorReasonDisplayer(errorReason)); 147 return; 148 } 149 150 // composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true); 151 int vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 152 composer = new VCardComposer(ExportVCardActivity.this, vcardType, true); 153 154 composer.addHandler(composer.new HandlerForOutputStream(outputStream)); 155 156 if (!composer.init()) { 157 final String errorReason = composer.getErrorReason(); 158 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason); 159 final String translatedErrorReason = 160 translateComposerError(errorReason); 161 mHandler.post(new ErrorReasonDisplayer( 162 getString(R.string.fail_reason_could_not_initialize_exporter, 163 translatedErrorReason))); 164 shouldCallFinish = false; 165 return; 166 } 167 168 int size = composer.getCount(); 169 170 if (size == 0) { 171 mHandler.post(new ErrorReasonDisplayer( 172 getString(R.string.fail_reason_no_exportable_contact))); 173 shouldCallFinish = false; 174 return; 175 } 176 177 mProgressDialog.setProgressNumberFormat( 178 getString(R.string.exporting_contact_list_progress)); 179 mProgressDialog.setMax(size); 180 mProgressDialog.setProgress(0); 181 182 while (!composer.isAfterLast()) { 183 if (mCanceled) { 184 return; 185 } 186 if (!composer.createOneEntry()) { 187 final String errorReason = composer.getErrorReason(); 188 Log.e(LOG_TAG, "Failed to read a contact: " + errorReason); 189 final String translatedErrorReason = 190 translateComposerError(errorReason); 191 mHandler.post(new ErrorReasonDisplayer( 192 getString(R.string.fail_reason_error_occurred_during_export, 193 translatedErrorReason))); 194 shouldCallFinish = false; 195 return; 196 } 197 mProgressDialog.incrementProgressBy(1); 198 } 199 } finally { 200 if (composer != null) { 201 composer.terminate(); 202 } 203 mWakeLock.release(); 204 mProgressDialog.dismiss(); 205 if (shouldCallFinish && !isFinishing()) { 206 finish(); 207 } 208 } 209 } 210 211 @Override 212 public void finalize() { 213 if (mWakeLock != null && mWakeLock.isHeld()) { 214 mWakeLock.release(); 215 } 216 } 217 218 public void cancel() { 219 mCanceled = true; 220 } 221 222 public void onCancel(DialogInterface dialog) { 223 cancel(); 224 } 225 } 226 227 private String translateComposerError(String errorMessage) { 228 Resources resources = getResources(); 229 if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) { 230 return resources.getString(R.string.composer_failed_to_get_database_infomation); 231 } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) { 232 return resources.getString(R.string.composer_has_no_exportable_contact); 233 } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) { 234 return resources.getString(R.string.composer_not_initialized); 235 } else { 236 return errorMessage; 237 } 238 } 239 240 @Override 241 protected void onCreate(Bundle bundle) { 242 super.onCreate(bundle); 243 244 mTargetDirectory = getString(R.string.config_export_dir); 245 mFileNamePrefix = getString(R.string.config_export_file_prefix); 246 mFileNameSuffix = getString(R.string.config_export_file_suffix); 247 mFileNameExtension = getString(R.string.config_export_file_extension); 248 mVCardTypeStr = getString(R.string.config_export_vcard_type); 249 250 mExtensionsToConsider = new HashSet<String>(); 251 mExtensionsToConsider.add(mFileNameExtension); 252 253 final String additionalExtensions = 254 getString(R.string.config_export_extensions_to_consider); 255 if (!TextUtils.isEmpty(additionalExtensions)) { 256 for (String extension : additionalExtensions.split(",")) { 257 String trimed = extension.trim(); 258 if (trimed.length() > 0) { 259 mExtensionsToConsider.add(trimed); 260 } 261 } 262 } 263 264 final Resources resources = getResources(); 265 mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index); 266 mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index); 267 268 startExportVCardToSdCard(); 269 } 270 271 @Override 272 protected Dialog onCreateDialog(int id) { 273 switch (id) { 274 case R.id.dialog_export_confirmation: { 275 return getExportConfirmationDialog(); 276 } 277 case R.string.fail_reason_too_many_vcard: { 278 return new AlertDialog.Builder(this) 279 .setTitle(R.string.exporting_contact_failed_title) 280 .setMessage(getString(R.string.exporting_contact_failed_message, 281 getString(R.string.fail_reason_too_many_vcard))) 282 .setPositiveButton(android.R.string.ok, mCancelListener) 283 .create(); 284 } 285 case R.id.dialog_fail_to_export_with_reason: { 286 return getErrorDialogWithReason(); 287 } 288 case R.id.dialog_sdcard_not_found: { 289 AlertDialog.Builder builder = new AlertDialog.Builder(this) 290 .setTitle(R.string.no_sdcard_title) 291 .setIcon(android.R.drawable.ic_dialog_alert) 292 .setMessage(R.string.no_sdcard_message) 293 .setPositiveButton(android.R.string.ok, mCancelListener); 294 return builder.create(); 295 } 296 case R.id.dialog_exporting_vcard: { 297 if (mProgressDialog == null) { 298 String title = getString(R.string.exporting_contact_list_title); 299 String message = getString(R.string.exporting_contact_list_message, 300 mExportingFileName); 301 mProgressDialog = new ProgressDialog(ExportVCardActivity.this); 302 mProgressDialog.setTitle(title); 303 mProgressDialog.setMessage(message); 304 mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 305 mProgressDialog.setOnCancelListener(mActualExportThread); 306 mActualExportThread.start(); 307 } 308 return mProgressDialog; 309 } 310 } 311 return super.onCreateDialog(id); 312 } 313 314 @Override 315 protected void onPrepareDialog(int id, Dialog dialog) { 316 if (id == R.id.dialog_fail_to_export_with_reason) { 317 ((AlertDialog)dialog).setMessage(getErrorReason()); 318 } else if (id == R.id.dialog_export_confirmation) { 319 ((AlertDialog)dialog).setMessage( 320 getString(R.string.confirm_export_message, mTargetFileName)); 321 } else { 322 super.onPrepareDialog(id, dialog); 323 } 324 } 325 326 @Override 327 protected void onStop() { 328 super.onStop(); 329 if (mActualExportThread != null) { 330 // The Activity is no longer visible. Stop the thread. 331 mActualExportThread.cancel(); 332 mActualExportThread = null; 333 } 334 335 if (!isFinishing()) { 336 finish(); 337 } 338 } 339 340 /** 341 * Tries to start exporting VCard. If there's no SDCard available, 342 * an error dialog is shown. 343 */ 344 public void startExportVCardToSdCard() { 345 File targetDirectory = new File(mTargetDirectory); 346 347 if (!(targetDirectory.exists() && 348 targetDirectory.isDirectory() && 349 targetDirectory.canRead()) && 350 !targetDirectory.mkdirs()) { 351 showDialog(R.id.dialog_sdcard_not_found); 352 } else { 353 mTargetFileName = getAppropriateFileName(mTargetDirectory); 354 if (TextUtils.isEmpty(mTargetFileName)) { 355 mTargetFileName = null; 356 // finish() is called via the error dialog. Do not call the method here. 357 return; 358 } 359 360 showDialog(R.id.dialog_export_confirmation); 361 } 362 } 363 364 /** 365 * Tries to get an appropriate filename. Returns null if it fails. 366 */ 367 private String getAppropriateFileName(final String destDirectory) { 368 int fileNumberStringLength = 0; 369 { 370 // Calling Math.Log10() is costly. 371 int tmp; 372 for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0; 373 fileNumberStringLength++, tmp /= 10) { 374 } 375 } 376 String bodyFormat = "%s%0" + fileNumberStringLength + "d%s"; 377 378 if (!ALLOW_LONG_FILE_NAME) { 379 String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix); 380 if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) { 381 Log.e(LOG_TAG, "This code does not allow any long file name."); 382 mErrorReason = getString(R.string.fail_reason_too_long_filename, 383 String.format("%s.%s", possibleBody, mFileNameExtension)); 384 showDialog(R.id.dialog_fail_to_export_with_reason); 385 // finish() is called via the error dialog. Do not call the method here. 386 return null; 387 } 388 } 389 390 // Note that this logic assumes that the target directory is case insensitive. 391 // As of 2009-07-16, it is true since the external storage is only sdcard, and 392 // it is formated as FAT/VFAT. 393 // TODO: fix this. 394 for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) { 395 boolean numberIsAvailable = true; 396 // SD Association's specification seems to require this feature, though we cannot 397 // have the specification since it is proprietary... 398 String body = null; 399 for (String possibleExtension : mExtensionsToConsider) { 400 body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix); 401 File file = new File(String.format("%s/%s.%s", 402 destDirectory, body, possibleExtension)); 403 if (file.exists()) { 404 numberIsAvailable = false; 405 break; 406 } 407 } 408 if (numberIsAvailable) { 409 return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension); 410 } 411 } 412 showDialog(R.string.fail_reason_too_many_vcard); 413 return null; 414 } 415 416 public Dialog getExportConfirmationDialog() { 417 if (TextUtils.isEmpty(mTargetFileName)) { 418 Log.e(LOG_TAG, "Target file name is empty, which must not be!"); 419 // This situation is not acceptable (probably a bug!), but we don't have no reason to 420 // show... 421 mErrorReason = null; 422 return getErrorDialogWithReason(); 423 } 424 425 return new AlertDialog.Builder(this) 426 .setTitle(R.string.confirm_export_title) 427 .setMessage(getString(R.string.confirm_export_message, mTargetFileName)) 428 .setPositiveButton(android.R.string.ok, 429 new ExportConfirmationListener(mTargetFileName)) 430 .setNegativeButton(android.R.string.cancel, mCancelListener) 431 .setOnCancelListener(mCancelListener) 432 .create(); 433 } 434 435 public Dialog getErrorDialogWithReason() { 436 if (mErrorReason == null) { 437 Log.e(LOG_TAG, "Error reason must have been set."); 438 mErrorReason = getString(R.string.fail_reason_unknown); 439 } 440 return new AlertDialog.Builder(this) 441 .setTitle(R.string.exporting_contact_failed_title) 442 .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason)) 443 .setPositiveButton(android.R.string.ok, mCancelListener) 444 .setOnCancelListener(mCancelListener) 445 .create(); 446 } 447 448 public void cancelExport() { 449 if (mActualExportThread != null) { 450 mActualExportThread.cancel(); 451 mActualExportThread = null; 452 } 453 } 454 455 public String getErrorReason() { 456 return mErrorReason; 457 } 458 }