package com.ichi2.anki.dialogs;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Message;
import android.view.View;
import com.afollestad.materialdialogs.MaterialDialog;
import com.ichi2.anki.AnkiActivity;
import com.ichi2.anki.BackupManager;
import com.ichi2.anki.CollectionHelper;
import com.ichi2.anki.DeckPicker;
import com.ichi2.anki.R;
import com.ichi2.compat.CompatHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
public class DatabaseErrorDialog extends AsyncDialogFragment {
private int mType = 0;
private int[] mRepairValues;
private File[] mBackups;
public static final int DIALOG_LOAD_FAILED = 0;
public static final int DIALOG_DB_ERROR = 1;
public static final int DIALOG_ERROR_HANDLING = 2;
public static final int DIALOG_REPAIR_COLLECTION = 3;
public static final int DIALOG_RESTORE_BACKUP = 4;
public static final int DIALOG_NEW_COLLECTION = 5;
public static final int DIALOG_CONFIRM_DATABASE_CHECK = 6;
public static final int DIALOG_CONFIRM_RESTORE_BACKUP = 7;
public static final int DIALOG_FULL_SYNC_FROM_SERVER = 8;
public static final int DIALOG_CURSOR_SIZE_LIMIT_EXCEEDED = 9;
// public flag which lets us distinguish between inaccessible and corrupt database
public static boolean databaseCorruptFlag = false;
/**
* A set of dialogs which deal with problems with the database when it can't load
*
* @param dialogType An integer which specifies which of the sub-dialogs to show
*/
public static DatabaseErrorDialog newInstance(int dialogType) {
DatabaseErrorDialog f = new DatabaseErrorDialog();
Bundle args = new Bundle();
args.putInt("dialogType", dialogType);
f.setArguments(args);
return f;
}
@Override
public MaterialDialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mType = getArguments().getInt("dialogType");
Resources res = getResources();
MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity());
builder.cancelable(true)
.title(getTitle());
boolean sqliteInstalled = false;
try {
sqliteInstalled = Runtime.getRuntime().exec("sqlite3 --version").waitFor() == 0;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
switch (mType) {
case DIALOG_CURSOR_SIZE_LIMIT_EXCEEDED:
case DIALOG_LOAD_FAILED:
// Collection failed to load; give user the option of either choosing from repair options, or closing
// the activity
return builder.cancelable(false)
.content(getMessage())
.iconAttr(R.attr.dialogErrorIcon)
.positiveText(res.getString(R.string.error_handling_options))
.negativeText(res.getString(R.string.close))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_ERROR_HANDLING);
}
@Override
public void onNegative(MaterialDialog dialog) {
((DeckPicker) getActivity()).exit();
}
})
.show();
case DIALOG_DB_ERROR:
// Database Check failed to execute successfully; give user the option of either choosing from repair
// options, submitting an error report, or closing the activity
MaterialDialog dialog = builder
.cancelable(false)
.content(getMessage())
.iconAttr(R.attr.dialogErrorIcon)
.positiveText(res.getString(R.string.error_handling_options))
.negativeText(res.getString(R.string.answering_error_report))
.neutralText(res.getString(R.string.close))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_ERROR_HANDLING);
}
@Override
public void onNegative(MaterialDialog dialog) {
((DeckPicker) getActivity()).sendErrorReport();
dismissAllDialogFragments();
}
@Override
public void onNeutral(MaterialDialog dialog) {
((DeckPicker) getActivity()).exit();
}
})
.show();
dialog.getCustomView().findViewById(R.id.buttonDefaultNegative).setEnabled(
((DeckPicker) getActivity()).hasErrorFiles());
return dialog;
case DIALOG_ERROR_HANDLING:
// The user has asked to see repair options; allow them to choose one of the repair options or go back
// to the previous dialog
ArrayList<String> options = new ArrayList<>();
ArrayList<Integer> values = new ArrayList<>();
if (!((AnkiActivity)getActivity()).colIsOpen()) {
// retry
options.add(res.getString(R.string.backup_retry_opening));
values.add(0);
} else {
// fix integrity
options.add(res.getString(R.string.check_db));
values.add(1);
}
// repair db with sqlite
if (sqliteInstalled) {
options.add(res.getString(R.string.backup_error_menu_repair));
values.add(2);
}
// // restore from backup
options.add(res.getString(R.string.backup_restore));
values.add(3);
// delete old collection and build new one
options.add(res.getString(R.string.backup_full_sync_from_server));
values.add(4);
// delete old collection and build new one
options.add(res.getString(R.string.backup_del_collection));
values.add(5);
String[] titles = new String[options.size()];
mRepairValues = new int[options.size()];
for (int i = 0; i < options.size(); i++) {
titles[i] = options.get(i);
mRepairValues[i] = values.get(i);
}
dialog = builder.iconAttr(R.attr.dialogErrorIcon)
.negativeText(res.getString(R.string.dialog_cancel))
.items(titles)
.itemsCallback(new MaterialDialog.ListCallback() {
@Override
public void onSelection(MaterialDialog materialDialog, View view, int which,
CharSequence charSequence) {
switch (mRepairValues[which]) {
case 0:
((DeckPicker) getActivity()).restartActivity();
return;
case 1:
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_CONFIRM_DATABASE_CHECK);
return;
case 2:
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_REPAIR_COLLECTION);
return;
case 3:
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_RESTORE_BACKUP);
return;
case 4:
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_FULL_SYNC_FROM_SERVER);
return;
case 5:
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_NEW_COLLECTION);
}
}
})
.show();
return dialog;
case DIALOG_REPAIR_COLLECTION:
// Allow user to run BackupManager.repairCollection()
return builder.content(getMessage())
.iconAttr(R.attr.dialogErrorIcon)
.positiveText(res.getString(R.string.dialog_positive_repair))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity()).repairDeck();
dismissAllDialogFragments();
}
})
.show();
case DIALOG_RESTORE_BACKUP:
// Allow user to restore one of the backups
String path = CollectionHelper.getInstance().getCollectionPath(getActivity());
File[] files = BackupManager.getBackups(new File(path));
mBackups = new File[files.length];
for (int i = 0; i < files.length; i++) {
mBackups[i] = files[files.length - 1 - i];
}
if (mBackups.length == 0) {
builder.title(res.getString(R.string.backup_restore))
.content(getMessage())
.positiveText(res.getString(R.string.dialog_ok))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_ERROR_HANDLING);
}
});
} else {
String[] dates = new String[mBackups.length];
for (int i = 0; i < mBackups.length; i++) {
dates[i] = mBackups[i].getName().replaceAll(
".*-(\\d{4}-\\d{2}-\\d{2})-(\\d{2})-(\\d{2}).apkg", "$1 ($2:$3 h)");
}
builder.title(res.getString(R.string.backup_restore_select_title))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onNegative(MaterialDialog dialog) {
dismissAllDialogFragments();
}
})
.items(dates)
.itemsCallbackSingleChoice(dates.length,
new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog materialDialog, View view,
int which, CharSequence charSequence) {
if (mBackups[which].length() > 0) {
// restore the backup if it's valid
((DeckPicker) getActivity())
.restoreFromBackup(mBackups[which]
.getPath());
dismissAllDialogFragments();
} else {
// otherwise show an error dialog
new MaterialDialog.Builder(getActivity())
.title(R.string.backup_error)
.content(R.string.backup_invalid_file_error)
.positiveText(R.string.dialog_ok)
.build().show();
}
return true;
}
});
}
return builder.show();
case DIALOG_NEW_COLLECTION:
// Allow user to create a new empty collection
return builder.content(getMessage())
.positiveText(res.getString(R.string.dialog_positive_create))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
CollectionHelper.getInstance().closeCollection(false);
String path = CollectionHelper.getCollectionPath(getActivity());
if (BackupManager.moveDatabaseToBrokenFolder(path, false)) {
((DeckPicker) getActivity()).restartActivity();
} else {
((DeckPicker) getActivity()).showDatabaseErrorDialog(DIALOG_LOAD_FAILED);
}
}
})
.show();
case DIALOG_CONFIRM_DATABASE_CHECK:
// Confirmation dialog for database check
return builder.content(getMessage())
.positiveText(res.getString(R.string.dialog_ok))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity()).integrityCheck();
dismissAllDialogFragments();
}
})
.show();
case DIALOG_CONFIRM_RESTORE_BACKUP:
// Confirmation dialog for backup restore
return builder.content(getMessage())
.positiveText(res.getString(R.string.dialog_continue))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity())
.showDatabaseErrorDialog(DIALOG_RESTORE_BACKUP);
}
})
.show();
case DIALOG_FULL_SYNC_FROM_SERVER:
// Allow user to do a full-sync from the server
return builder.content(getMessage())
.positiveText(res.getString(R.string.dialog_positive_overwrite))
.negativeText(res.getString(R.string.dialog_cancel))
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
((DeckPicker) getActivity()).sync("download");
dismissAllDialogFragments();
}
})
.show();
default:
return null;
}
}
private String getMessage() {
switch (getArguments().getInt("dialogType")) {
case DIALOG_LOAD_FAILED:
if (!CompatHelper.isHoneycomb()) {
// Before honeycomb there's no way to know if the db has actually been corrupted
// so we show a non-specific message.
return res().getString(R.string.open_collection_failed_message, res().getString(R.string.repair_deck));
} else if (databaseCorruptFlag) {
// The sqlite database has been corrupted (DatabaseErrorHandler.onCorrupt() was called)
// Show a specific message appropriate for the situation
return res().getString(R.string.corrupt_db_message, res().getString(R.string.repair_deck));
} else {
// Generic message shown when a libanki task failed
return res().getString(R.string.access_collection_failed_message, res().getString(R.string.link_help));
}
case DIALOG_DB_ERROR:
return res().getString(R.string.answering_error_message);
case DIALOG_REPAIR_COLLECTION:
return res().getString(R.string.repair_deck_dialog, BackupManager.BROKEN_DECKS_SUFFIX);
case DIALOG_RESTORE_BACKUP:
return res().getString(R.string.backup_restore_no_backups);
case DIALOG_NEW_COLLECTION:
return res().getString(R.string.backup_del_collection_question);
case DIALOG_CONFIRM_DATABASE_CHECK:
return res().getString(R.string.check_db_warning);
case DIALOG_CONFIRM_RESTORE_BACKUP:
return res().getString(R.string.restore_backup);
case DIALOG_FULL_SYNC_FROM_SERVER:
return res().getString(R.string.backup_full_sync_from_server_question);
case DIALOG_CURSOR_SIZE_LIMIT_EXCEEDED:
return res().getString(R.string.cursor_size_limit_exceeded);
default:
return getArguments().getString("dialogMessage");
}
}
private String getTitle() {
switch (getArguments().getInt("dialogType")) {
case DIALOG_LOAD_FAILED:
return res().getString(R.string.open_collection_failed_title);
case DIALOG_DB_ERROR:
return res().getString(R.string.answering_error_title);
case DIALOG_ERROR_HANDLING:
return res().getString(R.string.error_handling_title);
case DIALOG_REPAIR_COLLECTION:
return res().getString(R.string.backup_repair_deck);
case DIALOG_RESTORE_BACKUP:
return res().getString(R.string.backup_restore);
case DIALOG_NEW_COLLECTION:
return res().getString(R.string.backup_new_collection);
case DIALOG_CONFIRM_DATABASE_CHECK:
return res().getString(R.string.check_db_title);
case DIALOG_CONFIRM_RESTORE_BACKUP:
return res().getString(R.string.restore_backup_title);
case DIALOG_FULL_SYNC_FROM_SERVER:
return res().getString(R.string.backup_full_sync_from_server);
case DIALOG_CURSOR_SIZE_LIMIT_EXCEEDED:
return res().getString(R.string.open_collection_failed_title);
default:
return res().getString(R.string.answering_error_title);
}
}
@Override
public String getNotificationMessage() {
switch (getArguments().getInt("dialogType")) {
default:
return getMessage();
}
}
@Override
public String getNotificationTitle() {
switch (getArguments().getInt("dialogType")) {
default:
return res().getString(R.string.answering_error_title);
}
}
@Override
public Message getDialogHandlerMessage() {
Message msg = Message.obtain();
msg.what = DialogHandler.MSG_SHOW_DATABASE_ERROR_DIALOG;
Bundle b = new Bundle();
b.putInt("dialogType", getArguments().getInt("dialogType"));
msg.setData(b);
return msg;
}
public void dismissAllDialogFragments() {
((DeckPicker) getActivity()).dismissAllDialogFragments();
}
}