/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.edu.tsinghua.hpc.tcontacts;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.Log;
import cn.edu.tsinghua.hpc.tcontacts.pim.VCardComposer;
public class ExportVCardActivity extends Activity {
private static final String LOG_TAG = "ExportVCardActivity";
// If true, VCardExporter is able to emits files longer than 8.3 format.
private static final boolean ALLOW_LONG_FILE_NAME = false;
private String mTargetDirectory;
private String mFileNamePrefix;
private String mFileNameSuffix;
private int mFileIndexMinimum;
private int mFileIndexMaximum;
private String mFileNameExtension;
private String mVCardTypeStr;
private Set<String> mExtensionsToConsider;
private ProgressDialog mProgressDialog;
private String mExportingFileName;
private Handler mHandler = new Handler();
// Used temporaly when asking users to confirm the file name
private String mTargetFileName;
// String for storing error reason temporaly.
private String mErrorReason;
private ActualExportThread mActualExportThread;
private class CancelListener implements DialogInterface.OnClickListener,
DialogInterface.OnCancelListener {
public void onClick(DialogInterface dialog, int which) {
finish();
}
public void onCancel(DialogInterface dialog) {
finish();
}
}
private CancelListener mCancelListener = new CancelListener();
private class ErrorReasonDisplayer implements Runnable {
private final int mResId;
public ErrorReasonDisplayer(int resId) {
mResId = resId;
}
public ErrorReasonDisplayer(String errorReason) {
mResId = R.id.dialog_fail_to_export_with_reason;
mErrorReason = errorReason;
}
public void run() {
// Show the Dialog only when the parent Activity is still alive.
if (!ExportVCardActivity.this.isFinishing()) {
showDialog(mResId);
}
}
}
private class ExportConfirmationListener implements
DialogInterface.OnClickListener {
private final String mFileName;
public ExportConfirmationListener(String fileName) {
mFileName = fileName;
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
mActualExportThread = new ActualExportThread(mFileName);
mActualExportThread.start();
showDialog(R.id.dialog_exporting_vcard);
}
}
}
private class ActualExportThread extends Thread implements
DialogInterface.OnCancelListener {
private PowerManager.WakeLock mWakeLock;
private boolean mCanceled = false;
public ActualExportThread(String fileName) {
mExportingFileName = fileName;
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_DIM_WAKE_LOCK
| PowerManager.ON_AFTER_RELEASE, LOG_TAG);
}
@Override
public void run() {
boolean shouldCallFinish = true;
mWakeLock.acquire();
VCardComposer composer = null;
try {
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(mExportingFileName);
} catch (FileNotFoundException e) {
final String errorReason = getString(
R.string.fail_reason_could_not_open_file,
mExportingFileName, e.getMessage());
shouldCallFinish = false;
mHandler.post(new ErrorReasonDisplayer(errorReason));
return;
}
composer = new VCardComposer(ExportVCardActivity.this,
mVCardTypeStr, true);
/*
* int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
* VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES); composer =
* new VCardComposer(ExportVCardActivity.this, vcardType, true);
*/
composer.addHandler(composer.new HandlerForOutputStream(
outputStream));
if (!composer.init()) {
final String errorReason = composer.getErrorReason();
Log.e(LOG_TAG, "initialization of vCard composer failed: "
+ errorReason);
final String translatedErrorReason = translateComposerError(errorReason);
mHandler.post(new ErrorReasonDisplayer(getString(
R.string.fail_reason_could_not_initialize_exporter,
translatedErrorReason)));
shouldCallFinish = false;
return;
}
int size = composer.getCount();
if (size == 0) {
mHandler
.post(new ErrorReasonDisplayer(
getString(R.string.fail_reason_no_exportable_contact)));
shouldCallFinish = false;
return;
}
try {
Method setProgressNumberFormatMethod = Class.forName(
"android.app.ProgressDialog").getMethod(
"setProgressNumberFormat",
new Class[] { String.class });
setProgressNumberFormatMethod.setAccessible(true);
setProgressNumberFormatMethod
.invoke(
mProgressDialog,
new Object[] { getString(R.string.exporting_contact_list_progress) });
} catch (Exception e) {
Log.d("TContact",e.getMessage());
}
// mProgressDialog.setProgressNumberFormat(
// getString(R.string.exporting_contact_list_progress));
mProgressDialog.setMax(size);
mProgressDialog.setProgress(0);
while (!composer.isAfterLast()) {
if (mCanceled) {
return;
}
if (!composer.createOneEntry()) {
final String errorReason = composer.getErrorReason();
Log.e(LOG_TAG, "Failed to read a contact: "
+ errorReason);
final String translatedErrorReason = translateComposerError(errorReason);
mHandler
.post(new ErrorReasonDisplayer(
getString(
R.string.fail_reason_error_occurred_during_export,
translatedErrorReason)));
shouldCallFinish = false;
return;
}
mProgressDialog.incrementProgressBy(1);
}
} finally {
if (composer != null) {
composer.terminate();
}
mWakeLock.release();
mProgressDialog.dismiss();
if (shouldCallFinish && !isFinishing()) {
finish();
}
}
}
@Override
public void finalize() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
}
}
public void cancel() {
mCanceled = true;
}
public void onCancel(DialogInterface dialog) {
cancel();
}
}
private String translateComposerError(String errorMessage) {
Resources resources = getResources();
if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO
.equals(errorMessage)) {
return resources
.getString(R.string.composer_failed_to_get_database_infomation);
} else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
return resources
.getString(R.string.composer_has_no_exportable_contact);
} else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED
.equals(errorMessage)) {
return resources.getString(R.string.composer_not_initialized);
} else {
return errorMessage;
}
}
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
mTargetDirectory = getString(R.string.config_export_dir);
mFileNamePrefix = getString(R.string.config_export_file_prefix);
mFileNameSuffix = getString(R.string.config_export_file_suffix);
mFileNameExtension = getString(R.string.config_export_file_extension);
mVCardTypeStr = getString(R.string.config_export_vcard_type);
mExtensionsToConsider = new HashSet<String>();
mExtensionsToConsider.add(mFileNameExtension);
final String additionalExtensions = getString(R.string.config_export_extensions_to_consider);
if (!TextUtils.isEmpty(additionalExtensions)) {
for (String extension : additionalExtensions.split(",")) {
String trimed = extension.trim();
if (trimed.length() > 0) {
mExtensionsToConsider.add(trimed);
}
}
}
final Resources resources = getResources();
mFileIndexMinimum = resources
.getInteger(R.integer.config_export_file_min_index);
mFileIndexMaximum = resources
.getInteger(R.integer.config_export_file_max_index);
startExportVCardToSdCard();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case R.id.dialog_export_confirmation: {
return getExportConfirmationDialog();
}
case R.string.fail_reason_too_many_vcard: {
return new AlertDialog.Builder(this).setTitle(
R.string.exporting_contact_failed_title).setMessage(
getString(R.string.exporting_contact_failed_message,
getString(R.string.fail_reason_too_many_vcard)))
.setPositiveButton(android.R.string.ok, mCancelListener)
.create();
}
case R.id.dialog_fail_to_export_with_reason: {
return getErrorDialogWithReason();
}
case R.id.dialog_sdcard_not_found: {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle(R.string.no_sdcard_title).setIcon(
android.R.drawable.ic_dialog_alert).setMessage(
R.string.no_sdcard_message).setPositiveButton(
android.R.string.ok, mCancelListener);
return builder.create();
}
case R.id.dialog_exporting_vcard: {
return getExportingVCardDialog();
}
}
return super.onCreateDialog(id);
}
private Dialog getExportingVCardDialog() {
if (mProgressDialog == null) {
String title = getString(R.string.exporting_contact_list_title);
String message = getString(R.string.exporting_contact_list_message,
mExportingFileName);
mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
mProgressDialog.setTitle(title);
mProgressDialog.setMessage(message);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setOnCancelListener(mActualExportThread);
}
return mProgressDialog;
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
if (id == R.id.dialog_fail_to_export_with_reason) {
((AlertDialog) dialog).setMessage(getErrorReason());
} else if (id == R.id.dialog_export_confirmation) {
((AlertDialog) dialog).setMessage(getString(
R.string.confirm_export_message, mTargetFileName));
} else {
super.onPrepareDialog(id, dialog);
}
}
@Override
protected void onStop() {
super.onStop();
if (mActualExportThread != null) {
// The Activity is no longer visible. Stop the thread.
mActualExportThread.cancel();
mActualExportThread = null;
}
if (!isFinishing()) {
finish();
}
}
/**
* Tries to start exporting VCard. If there's no SDCard available, an error
* dialog is shown.
*/
public void startExportVCardToSdCard() {
File targetDirectory = new File(mTargetDirectory);
if (!(targetDirectory.exists() && targetDirectory.isDirectory() && targetDirectory
.canRead())
&& !targetDirectory.mkdirs()) {
showDialog(R.id.dialog_sdcard_not_found);
} else {
mTargetFileName = getAppropriateFileName(mTargetDirectory);
if (TextUtils.isEmpty(mTargetFileName)) {
mTargetFileName = null;
// finish() is called via the error dialog. Do not call the
// method here.
return;
}
showDialog(R.id.dialog_export_confirmation);
}
}
/**
* Tries to get an appropriate filename. Returns null if it fails.
*/
private String getAppropriateFileName(final String destDirectory) {
int fileNumberStringLength = 0;
{
// Calling Math.Log10() is costly.
int tmp;
for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0; fileNumberStringLength++, tmp /= 10) {
}
}
String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
if (!ALLOW_LONG_FILE_NAME) {
String possibleBody = String.format(bodyFormat, mFileNamePrefix, 1,
mFileNameSuffix);
if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
Log.e(LOG_TAG, "This code does not allow any long file name.");
mErrorReason = getString(
R.string.fail_reason_too_long_filename, String.format(
"%s.%s", possibleBody, mFileNameExtension));
showDialog(R.id.dialog_fail_to_export_with_reason);
// finish() is called via the error dialog. Do not call the
// method here.
return null;
}
}
// Note that this logic assumes that the target directory is case
// insensitive.
// As of 2009-07-16, it is true since the external storage is only
// sdcard, and
// it is formated as FAT/VFAT.
// TODO: fix this.
for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
boolean numberIsAvailable = true;
// SD Association's specification seems to require this feature,
// though we cannot
// have the specification since it is proprietary...
String body = null;
for (String possibleExtension : mExtensionsToConsider) {
body = String.format(bodyFormat, mFileNamePrefix, i,
mFileNameSuffix);
File file = new File(String.format("%s/%s.%s", destDirectory,
body, possibleExtension));
if (file.exists()) {
numberIsAvailable = false;
break;
}
}
if (numberIsAvailable) {
return String.format("%s/%s.%s", destDirectory, body,
mFileNameExtension);
}
}
showDialog(R.string.fail_reason_too_many_vcard);
return null;
}
public Dialog getExportConfirmationDialog() {
if (TextUtils.isEmpty(mTargetFileName)) {
Log.e(LOG_TAG, "Target file name is empty, which must not be!");
// This situation is not acceptable (probably a bug!), but we don't
// have no reason to
// show...
mErrorReason = null;
return getErrorDialogWithReason();
}
return new AlertDialog.Builder(this).setTitle(
R.string.confirm_export_title).setMessage(
getString(R.string.confirm_export_message, mTargetFileName))
.setPositiveButton(android.R.string.ok,
new ExportConfirmationListener(mTargetFileName))
.setNegativeButton(android.R.string.cancel, mCancelListener)
.setOnCancelListener(mCancelListener).create();
}
public Dialog getErrorDialogWithReason() {
if (mErrorReason == null) {
Log.e(LOG_TAG, "Error reason must have been set.");
mErrorReason = getString(R.string.fail_reason_unknown);
}
return new AlertDialog.Builder(this).setTitle(
R.string.exporting_contact_failed_title).setMessage(
getString(R.string.exporting_contact_failed_message,
mErrorReason)).setPositiveButton(android.R.string.ok,
mCancelListener).setOnCancelListener(mCancelListener).create();
}
public void cancelExport() {
if (mActualExportThread != null) {
mActualExportThread.cancel();
mActualExportThread = null;
}
}
public String getErrorReason() {
return mErrorReason;
}
}