1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-23 18:02:15 -05:00

Handle activity restarts due to configuration changes

Close existing dialogs before the activity is destroyed. Recreate them
and rewire AsyncTasks to the new activity instance after its creation.
This commit is contained in:
cketti 2011-10-01 20:11:14 +02:00
parent 6e1bf2965d
commit dfa97cd878
3 changed files with 377 additions and 130 deletions

View File

@ -1034,9 +1034,12 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="settings_import_encryption_password_prompt">Please enter the password you used when exporting your settings:</string>
<string name="settings_export_account">Export account settings</string>
<string name="settings_export_all">Export settings and accounts</string>
<string name="settings_import_dialog_title">Import</string>
<string name="settings_export_dialog_title">Export</string>
<string name="settings_import">Import settings</string>
<string name="settings_exporting">Exporting settings...</string>
<string name="settings_importing">Importing settings...</string>
<string name="settings_import_scanning_file">Scanning file...</string>
<string name="settings_export_success">Saved exported settings to <xliff:g id="filename">%s</xliff:g></string>
<string name="settings_import_success">Imported <xliff:g id="accounts">%s</xliff:g> from <xliff:g id="filename">%s</xliff:g></string>
<plurals name="settings_import_success">
@ -1049,9 +1052,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="settings_export_failed_header">Export failed</string>
<string name="settings_import_success_header">Import succeeded</string>
<string name="settings_import_failed_header">Import failed</string>
<string name="settings_unknown_version">Unable to handle file of version <xliff:g id="version">%s</xliff:g></string>
<string name="account_unavailable">Account \"<xliff:g id="account">%s</xliff:g>\" is unavailable; check storage</string>
</resources>

View File

@ -13,6 +13,7 @@ import java.util.concurrent.ConcurrentHashMap;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
@ -20,7 +21,6 @@ import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@ -58,6 +58,7 @@ import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.misc.ExtendedAsyncTask;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.AccountSetupBasics;
import com.fsck.k9.activity.setup.Prefs;
@ -104,6 +105,30 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
private SearchAccount integratedInboxAccount = null;
private FontSizes mFontSizes = K9.getFontSizes();
/**
* Contains a reference to a {@link ExtendedAsyncTask} while it is running.
*/
private ExtendedAsyncTask<Void, Void, Boolean> mAsyncTask;
/**
* Contains information about the currently displayed dialog (if available).
*
* <p>
* This object is returned from {@link #onRetainNonConfigurationInstance()} if a dialog is
* being displayed while the activity is being restarted. It is then used by the new activity
* instance to re-create that dialog.
* </p>
*/
private DialogInfo mDialogInfo;
/**
* Reference to the dialog currently being displayed (if available).
*
* @see #showDialog(int, String)
*/
private AlertDialog mDialog;
private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1;
class AccountsHandler extends Handler {
@ -310,34 +335,48 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
Intent intent = getIntent();
boolean startup = intent.getData() == null && intent.getBooleanExtra(EXTRA_STARTUP, true);
onNewIntent(intent);
if (startup && K9.startIntegratedInbox()) {
onOpenAccount(integratedInboxAccount);
finish();
return;
} else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) {
// fall through to "else" if !onOpenAccount()
finish();
} else {
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
requestWindowFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.accounts);
ListView listView = getListView();
listView.setOnItemClickListener(this);
listView.setItemsCanFocus(false);
listView.setEmptyView(findViewById(R.id.empty));
findViewById(R.id.next).setOnClickListener(this);
registerForContextMenu(listView);
if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) {
String accountUuid = icicle.getString("selectedContextAccount");
mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid);
}
restoreAccountStats(icicle);
return;
}
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
requestWindowFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.accounts);
ListView listView = getListView();
listView.setOnItemClickListener(this);
listView.setItemsCanFocus(false);
listView.setEmptyView(findViewById(R.id.empty));
findViewById(R.id.next).setOnClickListener(this);
registerForContextMenu(listView);
if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) {
String accountUuid = icicle.getString("selectedContextAccount");
mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid);
}
restoreAccountStats(icicle);
// Handle activity restarts because of a configuration change (e.g. rotating the screen)
Object retained = getLastNonConfigurationInstance();
if (retained != null) {
// If we displayed a dialog before the configuration change, re-create it here
if (retained instanceof DialogInfo) {
DialogInfo dialogInfo = (DialogInfo) retained;
showDialog(dialogInfo.headerRes, dialogInfo.message);
}
// If there's an ExtendedAsyncTask running, update it with the new Activity
else if (retained instanceof ExtendedAsyncTask) {
mAsyncTask = (ExtendedAsyncTask) retained;
mAsyncTask.attach(this);
}
}
}
@SuppressWarnings("unchecked")
@ -389,6 +428,22 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
/**
* Save the reference to a currently displayed dialog or a running AsyncTask (if available).
*/
@Override
public Object onRetainNonConfigurationInstance() {
Object retain = null;
if (mDialogInfo != null) {
retain = mDialogInfo;
dismissDialog();
} else if (mAsyncTask != null) {
retain = mAsyncTask;
mAsyncTask.detach();
}
return retain;
}
private void refresh() {
BaseAccount[] accounts = Preferences.getPreferences(this).getAccounts();
@ -868,28 +923,73 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
Log.i(K9.LOG_TAG, "onImport importing from URI " + uri.toString());
new ListImportContentsAsyncTask(uri, null).execute();
mAsyncTask = new ListImportContentsAsyncTask(this, uri, null);
mAsyncTask.execute();
}
private void showDialog(final Context context, final int headerRes, final String message) {
this.runOnUiThread(new Runnable() {
private void asyncTaskFinished() {
mAsyncTask = null;
}
/**
* Stores information about a dialog.
*
* @see Accounts#showDialog(int, String)
* @see Accounts#onCreate(Bundle)
*/
private static class DialogInfo {
public final int headerRes;
//TODO: "message" is already localized. This is a problem if the activity is restarted when
// the system language was changed. We have to recreate the message string in that case.
public final String message;
DialogInfo(int headerRes, String message) {
this.headerRes = headerRes;
this.message = message;
}
}
/**
* Show a dialog.
*
* @param headerRes
* The resource ID of the string that is used as title for the dialog box.
* @param message
* The message to display.
*/
private void showDialog(final int headerRes, final String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
// Store information about the dialog so it can be re-created when the activity is
// restarted due to a configuration change.
mDialogInfo = new DialogInfo(headerRes, message);
final AlertDialog.Builder builder = new AlertDialog.Builder(Accounts.this);
builder.setTitle(headerRes);
builder.setMessage(message);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
dismissDialog();
}
});
builder.show();
mDialog = builder.show();
}
});
}
/**
* Dismiss the dialog that was created using {@link #showDialog(int, String)}.
*/
private void dismissDialog() {
mDialog.dismiss();
mDialogInfo = null;
mDialog = null;
}
class AccountsAdapter extends ArrayAdapter<BaseAccount> {
public AccountsAdapter(BaseAccount[] accounts) {
super(Accounts.this, 0, accounts);
@ -1112,34 +1212,39 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
})
.show();
*/
new ExportAsyncTask(includeGlobals, accountUuids, null).execute();
mAsyncTask = new ExportAsyncTask(this, includeGlobals, accountUuids, null);
mAsyncTask.execute();
}
private class ExportAsyncTask extends AsyncTask<Void, Void, Boolean> {
/**
* Handles exporting of global settings and/or accounts in a background thread.
*/
private static class ExportAsyncTask extends ExtendedAsyncTask<Void, Void, Boolean> {
private boolean mIncludeGlobals;
private Set<String> mAccountUuids;
private String mEncryptionKey;
private String mFileName;
private ExportAsyncTask(boolean includeGlobals, Set<String> accountUuids,
String encryptionKey) {
private ExportAsyncTask(Accounts activity, boolean includeGlobals,
Set<String> accountUuids, String encryptionKey) {
super(activity);
mIncludeGlobals = includeGlobals;
mAccountUuids = accountUuids;
mEncryptionKey = encryptionKey;
}
@Override
protected void onPreExecute() {
//TODO: show progress bar instead of displaying toast
String toastText = Accounts.this.getString(R.string.settings_exporting);
Toast toast = Toast.makeText(Accounts.this, toastText, Toast.LENGTH_SHORT);
toast.show();
protected void showProgressDialog() {
String title = mContext.getString(R.string.settings_export_dialog_title);
String message = mContext.getString(R.string.settings_exporting);
mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
}
@Override
protected Boolean doInBackground(Void... params) {
try {
mFileName = StorageExporter.exportToFile(Accounts.this, mIncludeGlobals,
mFileName = StorageExporter.exportToFile(mContext, mIncludeGlobals,
mAccountUuids, mEncryptionKey);
} catch (StorageImportExportException e) {
Log.w(K9.LOG_TAG, "Exception during export", e);
@ -1150,18 +1255,28 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
@Override
protected void onPostExecute(Boolean success) {
Accounts activity = (Accounts) mActivity;
// Let the activity know that the background task is complete
activity.asyncTaskFinished();
removeProgressDialog();
if (success) {
showDialog(Accounts.this, R.string.settings_export_success_header,
Accounts.this.getString(R.string.settings_export_success, mFileName));
activity.showDialog(R.string.settings_export_success_header,
mContext.getString(R.string.settings_export_success, mFileName));
} else {
//TODO: make the exporter return an error code; translate that error code to a localized string here
showDialog(Accounts.this, R.string.settings_export_failed_header,
Accounts.this.getString(R.string.settings_export_failure, "Something went wrong"));
activity.showDialog(R.string.settings_export_failed_header,
mContext.getString(R.string.settings_export_failure, "Something went wrong"));
}
}
}
private class ImportAsyncTask extends AsyncTask<Void, Void, Boolean> {
/**
* Handles importing of global settings and/or accounts in a background thread.
*/
private static class ImportAsyncTask extends ExtendedAsyncTask<Void, Void, Boolean> {
private boolean mIncludeGlobals;
private List<String> mAccountUuids;
private boolean mOverwrite;
@ -1169,8 +1284,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
private InputStream mInputStream;
private ImportResults mImportResults;
private ImportAsyncTask(boolean includeGlobals, List<String> accountUuids,
boolean overwrite, String encryptionKey, InputStream is) {
private ImportAsyncTask(Accounts activity, boolean includeGlobals,
List<String> accountUuids, boolean overwrite, String encryptionKey,
InputStream is) {
super(activity);
mIncludeGlobals = includeGlobals;
mAccountUuids = accountUuids;
mOverwrite = overwrite;
@ -1179,17 +1296,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
@Override
protected void onPreExecute() {
//TODO: show progress bar instead of displaying toast
String toastText = Accounts.this.getString(R.string.settings_importing);
Toast toast = Toast.makeText(Accounts.this, toastText, Toast.LENGTH_SHORT);
toast.show();
protected void showProgressDialog() {
String title = mContext.getString(R.string.settings_import_dialog_title);
String message = mContext.getString(R.string.settings_importing);
mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
}
@Override
protected Boolean doInBackground(Void... params) {
try {
mImportResults = StorageImporter.importSettings(Accounts.this, mInputStream,
mImportResults = StorageImporter.importSettings(mContext, mInputStream,
mEncryptionKey, mIncludeGlobals, mAccountUuids, mOverwrite);
} catch (StorageImportExportException e) {
Log.w(K9.LOG_TAG, "Exception during export", e);
@ -1200,49 +1316,60 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
@Override
protected void onPostExecute(Boolean success) {
Accounts activity = (Accounts) mActivity;
// Let the activity know that the background task is complete
activity.asyncTaskFinished();
removeProgressDialog();
if (success) {
int imported = mImportResults.importedAccounts.size();
//TODO: display names of imported accounts (name from file *and* possibly new name)
showDialog(Accounts.this, R.string.settings_import_success_header,
activity.showDialog(R.string.settings_import_success_header,
//FIXME: use correct file name
Accounts.this.getString(R.string.settings_import_success, imported, "filename"));
refresh();
mContext.getString(R.string.settings_import_success, imported, "filename"));
activity.refresh();
} else {
//TODO: make the importer return an error code; translate that error code to a localized string here
showDialog(Accounts.this, R.string.settings_import_failed_header,
Accounts.this.getString(R.string.settings_import_failure, "unknown", "Something went wrong"));
activity.showDialog(R.string.settings_import_failed_header,
mContext.getString(R.string.settings_import_failure, "unknown", "Something went wrong"));
}
}
}
ImportContents mImportContents;
private class ListImportContentsAsyncTask extends AsyncTask<Void, Void, Boolean> {
private static class ListImportContentsAsyncTask extends ExtendedAsyncTask<Void, Void, Boolean> {
private Uri mUri;
private String mEncryptionKey;
private InputStream mInputStream;
private ImportContents mImportContents;
private ListImportContentsAsyncTask(Accounts activity, Uri uri, String encryptionKey) {
super(activity);
private ListImportContentsAsyncTask(Uri uri, String encryptionKey) {
mUri = uri;
mEncryptionKey = encryptionKey;
}
@Override
protected void onPreExecute() {
//TODO: show progress bar
protected void showProgressDialog() {
String title = mContext.getString(R.string.settings_import_dialog_title);
String message = mContext.getString(R.string.settings_import_scanning_file);
mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
}
@Override
protected Boolean doInBackground(Void... params) {
try {
InputStream is = getContentResolver().openInputStream(mUri);
mImportContents = StorageImporter.getImportStreamContents(
Accounts.this, is, mEncryptionKey);
ContentResolver resolver = mContext.getContentResolver();
InputStream is = resolver.openInputStream(mUri);
mImportContents = StorageImporter.getImportStreamContents(mContext, is,
mEncryptionKey);
// Open another InputStream in the background. This is used later by ImportAsyncTask
mInputStream = getContentResolver().openInputStream(mUri);
mInputStream = resolver.openInputStream(mUri);
} catch (StorageImportExportException e) {
Log.w(K9.LOG_TAG, "Exception during export", e);
@ -1257,80 +1384,96 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
@Override
protected void onPostExecute(Boolean success) {
Accounts activity = (Accounts) mActivity;
// Let the activity know that the background task is complete
activity.asyncTaskFinished();
removeProgressDialog();
if (success) {
final ListView importSelectionView = new ListView(Accounts.this);
List<String> contents = new ArrayList<String>();
if (mImportContents.globalSettings) {
contents.add("Global settings");
showImportSelectionDialog();
} else {
//TODO: make the importer return an error code; translate that error code to a localized string here
activity.showDialog(R.string.settings_import_failed_header,
mContext.getString(R.string.settings_import_failure, "unknown", "Something went wrong"));
}
}
//TODO: we need to be able to re-create this dialog after a configuration change
private void showImportSelectionDialog() {
final ListView importSelectionView = new ListView(mActivity);
List<String> contents = new ArrayList<String>();
if (mImportContents.globalSettings) {
contents.add("Global settings");
}
for (AccountDescription account : mImportContents.accounts) {
contents.add(account.name);
}
importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
importSelectionView.setAdapter(new ArrayAdapter<String>(mActivity, android.R.layout.simple_list_item_checked, contents));
importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
CheckedTextView ctv = (CheckedTextView)view;
ctv.setChecked(!ctv.isChecked());
}
for (AccountDescription account : mImportContents.accounts) {
contents.add(account.name);
}
importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
importSelectionView.setAdapter(new ArrayAdapter<String>(Accounts.this, android.R.layout.simple_list_item_checked, contents));
importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onNothingSelected(AdapterView<?> arg0) { /* Do nothing */ }
});
//TODO: listview header: "Please select the settings you wish to import"
//TODO: listview footer: "Select all" / "Select none" buttons?
//TODO: listview footer: "Overwrite existing accounts?" checkbox
final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle("Import selection");
builder.setView(importSelectionView);
builder.setInverseBackgroundForced(true);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
CheckedTextView ctv = (CheckedTextView)view;
ctv.setChecked(!ctv.isChecked());
public void onClick(DialogInterface dialog, int which) {
ListAdapter adapter = importSelectionView.getAdapter();
int count = adapter.getCount();
SparseBooleanArray pos = importSelectionView.getCheckedItemPositions();
boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false;
List<String> accountUuids = new ArrayList<String>();
int start = mImportContents.globalSettings ? 1 : 0;
for (int i = start; i < count; i++) {
if (pos.get(i)) {
accountUuids.add(mImportContents.accounts.get(i-start).uuid);
}
}
/*
* TODO: Think some more about this. Overwriting could change the store
* type. This requires some additional code in order to work smoothly
* while the app is running.
*/
boolean overwrite = false;
dialog.dismiss();
Accounts activity = (Accounts) mActivity;
ImportAsyncTask importAsyncTask = new ImportAsyncTask(activity, includeGlobals, accountUuids, overwrite, mEncryptionKey, mInputStream);
activity.mAsyncTask = importAsyncTask;
importAsyncTask.execute();
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {}
});
//TODO: listview header: "Please select the settings you wish to import"
//TODO: listview footer: "Select all" / "Select none" buttons?
//TODO: listview footer: "Overwrite existing accounts?" checkbox
final AlertDialog.Builder builder = new AlertDialog.Builder(Accounts.this);
builder.setTitle("Import selection");
builder.setView(importSelectionView);
builder.setInverseBackgroundForced(true);
builder.setPositiveButton(R.string.okay_action,
builder.setNegativeButton(R.string.cancel_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ListAdapter adapter = importSelectionView.getAdapter();
int count = adapter.getCount();
SparseBooleanArray pos = importSelectionView.getCheckedItemPositions();
boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false;
List<String> accountUuids = new ArrayList<String>();
int start = mImportContents.globalSettings ? 1 : 0;
for (int i = start; i < count; i++) {
if (pos.get(i)) {
accountUuids.add(mImportContents.accounts.get(i-start).uuid);
}
}
/*
* TODO: Think some more about this. Overwriting could change the store
* type. This requires some additional code in order to work smoothly
* while the app is running.
*/
boolean overwrite = false;
dialog.dismiss();
new ImportAsyncTask(includeGlobals, accountUuids, overwrite, mEncryptionKey, mInputStream).execute();
try {
mInputStream.close();
} catch (Exception e) { /* Ignore */ }
}
});
builder.setNegativeButton(R.string.cancel_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
try {
mInputStream.close();
} catch (Exception e) { /* Ignore */ }
}
});
builder.show();
} else {
//TODO: make the importer return an error code; translate that error code to a localized string here
showDialog(Accounts.this, R.string.settings_import_failed_header,
Accounts.this.getString(R.string.settings_import_failure, "unknown", "Something went wrong"));
}
builder.show();
}
}
}

View File

@ -0,0 +1,101 @@
package com.fsck.k9.activity.misc;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
/**
* Extends {@link AsyncTask} with methods to attach and detach an {@link Activity}.
*
* <p>
* This is necessary to properly handle configuration changes that will restart an activity.
* </p><p>
* <strong>Note:</strong>
* Implementing classes need to make sure they have no reference to the {@code Activity} instance
* that created the instance of that class. So if it's implemented as inner class, it needs to be
* {@code static}.
* </p>
*
* @param <Params>
* see {@link AsyncTask}
* @param <Progress>
* see {@link AsyncTask}
* @param <Result>
* see {@link AsyncTask}
*
* @see #attach(Activity)
* @see #detach()
*/
public abstract class ExtendedAsyncTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
protected Activity mActivity;
protected Context mContext;
protected ProgressDialog mProgressDialog;
protected ExtendedAsyncTask(Activity activity) {
mActivity = activity;
mContext = activity.getApplicationContext();
}
/**
* Connect this {@link AsyncTask} to a new {@link Activity} instance after the activity
* was restarted due to a configuration change.
*
* <p>
* This also creates a new progress dialog that is bound to the new activity.
* </p>
*
* @param activity
* The new {@code Activity} instance. Never {@code null}.
*/
public void attach(Activity activity) {
mActivity = activity;
showProgressDialog();
}
/**
* Detach this {@link AsyncTask} from the {@link Activity} it was bound to.
*
* <p>
* This needs to be called when the current activity is being destroyed during an activity
* restart due to a configuration change.<br/>
* We also have to destroy the progress dialog because it's bound to the activity that's
* being destroyed.
* </p>
*
* @see Activity#onRetainNonConfigurationInstance()
*/
public void detach() {
removeProgressDialog();
mActivity = null;
}
/**
* Creates a {@link ProgressDialog} that is shown while the background thread is running.
*
* <p>
* This needs to store a {@code ProgressDialog} instance in {@link #mProgressDialog} or
* override {@link #removeProgressDialog()}.
* </p>
*/
protected abstract void showProgressDialog();
protected void removeProgressDialog() {
mProgressDialog.dismiss();
mProgressDialog = null;
}
/**
* This default implementation only creates a progress dialog.
*
* <p>
* <strong>Important:</strong>
* Be sure to call {@link #removeProgressDialog()} in {@link AsyncTask#onPostExecute(Object)}.
* </p>
*/
@Override
protected void onPreExecute() {
showProgressDialog();
}
}