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