package com.door43.translationstudio.newui;
import android.app.Activity;
import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.view.MenuItemCompat;
import android.os.Bundle;
import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.door43.tools.reporting.Logger;
import com.door43.translationstudio.R;
import com.door43.translationstudio.SettingsActivity;
import com.door43.translationstudio.core.ImportUsfm;
import com.door43.translationstudio.core.MissingNameItem;
import com.door43.translationstudio.core.TargetLanguage;
import com.door43.translationstudio.core.TargetTranslation;
import com.door43.translationstudio.core.Translator;
import com.door43.translationstudio.dialogs.CustomAlertDialog;
import com.door43.translationstudio.newui.library.ServerLibraryActivity;
import com.door43.translationstudio.newui.library.Searchable;
import com.door43.translationstudio.AppContext;
import com.door43.translationstudio.newui.newtranslation.ProjectListFragment;
import com.door43.translationstudio.newui.newtranslation.TargetLanguageListFragment;
import com.door43.util.FileUtilities;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.Serializable;
/**
* Handles the workflow UI for importing a USFM file.
*/
public class ImportUsfmActivity extends BaseActivity implements TargetLanguageListFragment.OnItemClickListener, ProjectListFragment.OnItemClickListener {
public static final int RESULT_DUPLICATE = 2;
private static final String STATE_TARGET_LANGUAGE_ID = "state_target_language_id";
public static final int RESULT_ERROR = 3;
public static final String EXTRA_USFM_IMPORT_URI = "extra_usfm_import_uri";
public static final String EXTRA_USFM_IMPORT_FILE = "extra_usfm_import_file";
public static final String EXTRA_USFM_IMPORT_RESOURCE_FILE = "extra_usfm_import_resource_file";
public static final String STATE_USFM = "state_usfm";
public static final String STATE_CURRENT_STATE = "state_current_state";
public static final String STATE_PROMPT_NAME_COUNTER = "state_prompt_name_counter";
public static final String STATE_FINISH_SUCCESS = "state_finish_success";
private Searchable mFragment;
public static final String TAG = ImportUsfmActivity.class.getSimpleName();
private TargetLanguage mTargetLanguage;
private ProgressDialog mProgressDialog = null;
private Thread mUsfmImportThread = null;
private Counter mCount;
private MissingNameItem[] mMissingNameItems;
private ImportUsfm mUsfm;
private Handler mHand;
private eImportState mCurrentState = eImportState.needLanguage;
private CustomAlertDialog mStatusDialog;
private boolean mFinishedSuccess = false;
private boolean mShuttingDown = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_import_usfm);
if (findViewById(R.id.fragment_container) != null) {
if (savedInstanceState != null) {
mFragment = (Searchable) getFragmentManager().findFragmentById(R.id.fragment_container);
} else {
setActivityStateTo(eImportState.needLanguage);
}
}
}
/**
* process an USFM file using the selected language
*/
private void processUsfmFile() {
final Intent intent = getIntent();
final Bundle args = intent.getExtras();
mCurrentState = eImportState.processingFiles;
mHand = new Handler(Looper.getMainLooper());
mHand.post(new Runnable() {
@Override
public void run() {
mUsfm = new ImportUsfm(ImportUsfmActivity.this, mTargetLanguage);
setTitle(mUsfm.getLanguageTitle());
processUsfmWithProgress(intent, args);
}
});
}
/**
* process an USFM file using the selected language showing progress dialog
*
* @param intent
* @param args
*/
private void processUsfmWithProgress(final Intent intent, final Bundle args) {
showProgressDialog();
mUsfmImportThread = new Thread() {
@Override
public void run() {
boolean success = beginUsfmProcessing(intent, args);
mMissingNameItems = mUsfm.getBooksMissingNames();
if (mMissingNameItems.length > 0) { // if we need valid names
mCount = new Counter(mMissingNameItems.length);
usfmPromptForNextName();
} else {
usfmShowProcessingResults();
}
}
};
mUsfmImportThread.start();
}
/**
* creates and displays progress dialog if not yet created, otherwise reuses existing dialog
*/
private void showProgressDialog() {
if(mShuttingDown) {
return;
}
if (null == mProgressDialog) {
mHand = new Handler(Looper.getMainLooper());
mProgressDialog = new ProgressDialog(ImportUsfmActivity.this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setCancelable(false);
mProgressDialog.setCanceledOnTouchOutside(false);
mProgressDialog.setTitle(R.string.reading_usfm);
mProgressDialog.setMessage("");
mProgressDialog.setMax(100);
mProgressDialog.show();
mUsfm.setUpdateStatusListener(new ImportUsfm.UpdateStatusListener() {
@Override
public void statusUpdate(final String textStatus, final int percent) {
Logger.i(TAG, "Update " + textStatus + ", " + percent);
updateProcessUsfmProgress(textStatus, percent);
}
});
}
}
/**
* class to keep track of number of books left to prompt for resource ID
*/
private class Counter {
public int counter;
Counter(int initialCount) {
counter = initialCount;
}
public boolean isEmpty() {
return counter == 0;
}
public void setCount(int count) {
counter = count;
}
public int increment() {
return ++counter;
}
public int decrement() {
if (counter > 0) {
counter--;
}
return counter;
}
}
/**
* will prompt for resource name of next book, or if done will move on to processing finish and import
*/
private void usfmPromptForNextName() {
if (mCount != null) {
if (mCount.isEmpty()) {
usfmShowProcessingResults();
return;
}
mHand.post(new Runnable() {
@Override
public void run() {
setActivityStateTo(eImportState.promptingForBookName);
}
});
}
}
/**
* will display prompt to user asking if they want to select the resource name for the book
*/
private void usfmPromptForName() {
if (mCount != null) {
mProgressDialog.hide();
int i = mCount.decrement();
final MissingNameItem item = mMissingNameItems[i];
String message = "";
final String description = mUsfm.getShortFilePath(item.description);
if (item.invalidName != null) {
String format = getResources().getString(R.string.invalid_book_name_prompt);
message = String.format(format, description, item.invalidName);
} else {
String format = getResources().getString(R.string.missing_book_name_prompt);
message = String.format(format, description);
}
mStatusDialog = CustomAlertDialog.Create(ImportUsfmActivity.this);
mStatusDialog.setTitle(R.string.title_activity_import_usfm_language)
.setMessage(message)
.setPositiveButton(R.string.label_continue, new View.OnClickListener() {
@Override
public void onClick(View v) {
mFragment = new ProjectListFragment();
((ProjectListFragment) mFragment).setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().replace(R.id.fragment_container, (ProjectListFragment) mFragment).commit();
String title = getResources().getString(R.string.title_activity_import_usfm_book);
title += " " + description;
setTitle(title);
mProgressDialog.hide();
}
})
.setNegativeButton(R.string.menu_cancel, new View.OnClickListener() {
@Override
public void onClick(View v) {
usfmPromptForNextName();
}
})
.setCancelableChainable(true)
.show("getName");
}
}
/**
* process selected book with specified resource name
*
* @param item
* @param resourceID
*/
private void usfmProcessBook(final MissingNameItem item, final String resourceID) {
Thread thread = new Thread() {
@Override
public void run() {
boolean success2 = mUsfm.processText(item.contents, item.description, false, resourceID);
Logger.i(TAG, resourceID + " success = " + success2);
usfmPromptForNextName();
}
};
thread.start();
}
/**
* processing of all books in file finished, show processing results and verify
* that user wants to import.
*/
private void usfmShowProcessingResults() {
if(mShuttingDown) {
return;
}
mCurrentState = eImportState.showingProcessingResults;
mHand.post(new Runnable() {
@Override
public void run() {
if (mProgressDialog != null) {
mProgressDialog.hide();
boolean processSuccess = mUsfm.isProcessSuccess();
String results = mUsfm.getResultsString();
String language = mUsfm.getLanguageTitle();
String message = language + "\n" + results;
View.OnClickListener continueListener = null;
mStatusDialog = CustomAlertDialog.Create(ImportUsfmActivity.this);
mStatusDialog.setTitle(processSuccess ? R.string.title_processing_usfm_summary : R.string.title_import_usfm_error)
.setMessage(message)
.setNegativeButton(R.string.menu_cancel, new View.OnClickListener() {
@Override
public void onClick(View v) {
usfmImportDone(true);
}
});
if(processSuccess) { // only show continue if successful processing
mStatusDialog.setPositiveButton(R.string.label_continue, new View.OnClickListener() {
@Override
public void onClick(View v) {
mProgressDialog.show();
mProgressDialog.setProgress(0);
mProgressDialog.setTitle(R.string.reading_usfm);
mProgressDialog.setMessage("");
doImportingWithProgress();
}
});
}
mStatusDialog.show("USFMresults");
}
}
});
}
/**
* import has finished
*
* @param cancelled
*/
private void usfmImportDone(boolean cancelled) {
mCurrentState = eImportState.finished;
cleanupUsfmImport();
if (cancelled) {
cancelled();
} else {
finished();
}
}
/**
* do importing of found books with progress updates
*/
private void doImportingWithProgress() {
mCurrentState = eImportState.importingFiles;
Thread thread = new Thread() {
@Override
public void run() {
File[] imports = mUsfm.getImportProjects();
final Translator translator = AppContext.getTranslator();
int count = 0;
int size = imports.length;
final int numSteps = 4;
final float subStepSize = 100f / (float) numSteps / (float) size;
if(mProgressDialog != null) {
mProgressDialog.setTitle(R.string.importing_usfm);
}
boolean success = true;
try {
for (File newDir : imports) {
String filename = newDir.getName().toString();
float progress = 100f * count++ / (float) size;
updateImportProgress(filename, progress);
TargetTranslation newTargetTranslation = TargetTranslation.open(newDir);
if (newTargetTranslation != null) {
newTargetTranslation.commitSync();
updateImportProgress(filename, progress + subStepSize);
// TRICKY: the correct id is pulled from the manifest to avoid propagating bad folder names
String targetTranslationId = newTargetTranslation.getId();
File localDir = new File(translator.getPath(), targetTranslationId);
TargetTranslation localTargetTranslation = TargetTranslation.open(localDir);
if (localTargetTranslation != null) {
// commit local changes to history
if (localTargetTranslation != null) {
localTargetTranslation.commitSync();
}
updateImportProgress(filename, progress + 2*subStepSize);
// merge translations
try {
localTargetTranslation.merge(newDir);
} catch (Exception e) {
Logger.e(TAG, "Failed to merge import folder " + newDir.toString(), e);
success = false;
continue;
}
} else {
// import new translation
FileUtilities.safeDelete(localDir); // in case local was an invalid target translation
FileUtils.moveDirectory(newDir, localDir);
}
// update the generator info. TRICKY: we re-open to get the updated manifest.
TargetTranslation.updateGenerator(ImportUsfmActivity.this, TargetTranslation.open(localDir));
}
}
updateProcessUsfmProgress("", 100);
} catch (Exception e) {
Logger.e(TAG, "Failed to import folder " + imports.toString(), e);
success = false;
}
mFinishedSuccess = success;
mHand.post(new Runnable() {
@Override
public void run() {
usfmShowImportResults();
}
});
}
};
thread.start();
}
/**
* update the import progress dialog
* @param filename
* @param progress
*/
private void updateImportProgress(String filename, float progress) {
String format = getResources().getString(R.string.importing_file);
updateProcessUsfmProgress(String.format(format, filename), Math.round(progress));
}
/**
* show results of import
*/
private void usfmShowImportResults() {
if(mShuttingDown) {
return;
}
mCurrentState = eImportState.showingImportResults;
mStatusDialog = CustomAlertDialog.Create(ImportUsfmActivity.this);
mStatusDialog.setTitle(mFinishedSuccess ? R.string.title_import_usfm_results : R.string.title_import_usfm_error)
.setMessage(mFinishedSuccess ? R.string.import_usfm_success : R.string.import_usfm_failed)
.setPositiveButton(R.string.label_continue, new View.OnClickListener() {
@Override
public void onClick(View v) {
usfmImportDone(false);
}
})
.show("USFMImportResults");
cleanupUsfmImport();
}
/**
* called to display progress of USFM processing or importing
*
* @param textStatus
* @param percent
*/
private void updateProcessUsfmProgress(final String textStatus, final int percent) {
if (mHand != null) {
mHand.post(new Runnable() {
@Override
public void run() {
if (mProgressDialog != null) {
if (null != textStatus) {
mProgressDialog.setMessage(textStatus);
}
int percentStatus = percent;
if (percentStatus > 100) {
percentStatus = 100;
} else if (percentStatus < 0) {
percentStatus = 0;
}
mProgressDialog.setProgress(percentStatus);
}
}
});
}
}
/**
* begin USFM processing using type passed (URI, File, or resource)
*
* @param intent
* @param args
* @return
*/
private boolean beginUsfmProcessing(Intent intent, Bundle args) {
boolean success = false;
if (args.containsKey(EXTRA_USFM_IMPORT_URI)) {
String uriStr = args.getString(EXTRA_USFM_IMPORT_URI);
Uri uri = intent.getData();
success = mUsfm.readUri(uri);
} else if (args.containsKey(EXTRA_USFM_IMPORT_FILE)) {
Serializable serial = args.getSerializable(EXTRA_USFM_IMPORT_FILE);
File file = (File) serial;
success = mUsfm.readFile(file);
} else if (args.containsKey(EXTRA_USFM_IMPORT_RESOURCE_FILE)) {
String importResourceFile = args.getString(EXTRA_USFM_IMPORT_RESOURCE_FILE);
success = mUsfm.readResourceFile(this, importResourceFile);
}
return success;
}
/**
* begins activity to process and import a file
*
* @param context
* @param path
*/
public static void startActivityForFileImport(Activity context, File path) {
Intent intent = new Intent(context, ImportUsfmActivity.class);
intent.putExtra(EXTRA_USFM_IMPORT_FILE, path);
context.startActivity(intent);
}
/**
* begins an activity to process and import a Uri
*
* @param context
* @param uri
*/
public static void startActivityForUriImport(Activity context, Uri uri) {
Intent intent = new Intent(context, ImportUsfmActivity.class);
intent.putExtra(EXTRA_USFM_IMPORT_URI, uri.toString()); // flag that we are using Uri
intent.setData(uri); // only way to pass data since Uri does not serialize
context.startActivity(intent);
}
/**
* begins an activity to process and import a resource
*
* @param context
* @param resourceName
*/
public static void startActivityForResourceImport(Activity context, String resourceName) {
Intent intent = new Intent(context, ImportUsfmActivity.class);
intent.putExtra(EXTRA_USFM_IMPORT_RESOURCE_FILE, resourceName);
context.startActivity(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_new_target_translation, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (mFragment instanceof ProjectListFragment) {
menu.findItem(R.id.action_update).setVisible(true);
} else {
menu.findItem(R.id.action_update).setVisible(false);
}
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
final MenuItem searchMenuItem = menu.findItem(R.id.action_search);
final SearchView searchViewAction = (SearchView) MenuItemCompat.getActionView(searchMenuItem);
searchViewAction.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
return true;
}
@Override
public boolean onQueryTextChange(String s) {
mFragment.onSearchQuery(s);
return true;
}
});
searchViewAction.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_settings:
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
case R.id.action_search:
return true;
case R.id.home:
onBackPressed();
return true;
case R.id.action_update:
CustomAlertDialog.Create(this)
.setTitle(R.string.update_projects)
.setIcon(R.drawable.ic_local_library_black_24dp)
.setMessage(R.string.use_internet_confirmation)
.setPositiveButton(R.string.yes, new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(ImportUsfmActivity.this, ServerLibraryActivity.class);
// intent.putExtra(ServerLibraryActivity.ARG_SHOW_UPDATES, true);
startActivity(intent);
}
})
.setNegativeButton(R.string.no, null)
.show("Update");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mShuttingDown = false;
if (savedInstanceState != null) {
String targetLanguageId = savedInstanceState.getString(STATE_TARGET_LANGUAGE_ID, null);
if (targetLanguageId != null) {
mTargetLanguage = AppContext.getLibrary().getTargetLanguage(targetLanguageId);
}
mCurrentState = eImportState.fromInt(savedInstanceState.getInt(STATE_CURRENT_STATE, eImportState.needLanguage.getValue()));
String usfmStr = savedInstanceState.getString(STATE_USFM, null);
if (usfmStr != null) {
mUsfm = ImportUsfm.newInstance(this, usfmStr);
}
if (savedInstanceState.containsKey(STATE_PROMPT_NAME_COUNTER) && (mUsfm != null)) {
int count = savedInstanceState.getInt(STATE_PROMPT_NAME_COUNTER);
mCount = new Counter(count + 1); // backup one
mMissingNameItems = mUsfm.getBooksMissingNames();
}
mFinishedSuccess = savedInstanceState.getBoolean(STATE_FINISH_SUCCESS, false);
mHand = new Handler(Looper.getMainLooper());
mHand.post(new Runnable() {
@Override
public void run() {
setActivityStateTo(mCurrentState);
}
});
}
}
/**
* update UI for specified state (e.g. prompt for language, book name selection, display processing results...)
* and begin that state
*
* @param currentState
*/
private void setActivityStateTo(eImportState currentState) {
if(mShuttingDown) {
return;
}
Logger.i(TAG, "setActivityStateTo(" + currentState + ")");
mCurrentState = currentState;
if (mUsfm != null) {
setTitle(mUsfm.getLanguageTitle());
}
switch (currentState) {
case needLanguage:
if (null == mFragment) {
mFragment = new TargetLanguageListFragment();
((TargetLanguageListFragment) mFragment).setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(R.id.fragment_container, (TargetLanguageListFragment) mFragment).commit();
// TODO: animate
}
break;
case processingFiles:
if((mUsfm != null) && (mTargetLanguage != null)) {
processUsfmFile();
break;
}
case promptingForBookName:
if (mCount != null) {
showProgressDialog();
usfmPromptForName();
break;
}
// otherwise we go down to showing results
case showingProcessingResults:
showProgressDialog();
usfmShowProcessingResults();
break;
case showingImportResults:
usfmShowImportResults();
break;
case importingFiles:
// not resumable - presume completed
mCurrentState = eImportState.finished;
case finished:
if(mUsfm != null) {
mUsfm.cleanup();
}
break;
}
}
@Override
public void onBackPressed() {
switch (mCurrentState) {
case needLanguage:
cancelled();
break;
case promptingForBookName:
setBook(null);
break;
case showingProcessingResults:
case processingFiles:
if (mUsfm != null) {
mUsfm.cleanup();
}
setActivityStateTo(eImportState.needLanguage);
break;
case showingImportResults:
usfmImportDone(false);
break;
default:
// not backup-able - presume completed
break;
}
}
public void onSaveInstanceState(Bundle outState) {
mShuttingDown = true;
if(mStatusDialog != null) {
mStatusDialog.dismiss();
}
mStatusDialog = null;
if(mProgressDialog != null) {
mProgressDialog.dismiss();
}
mProgressDialog = null;
eImportState currentState = mCurrentState; //capture state before it is changed
outState.putInt(STATE_CURRENT_STATE, currentState.getValue());
if (mTargetLanguage != null) {
outState.putString(STATE_TARGET_LANGUAGE_ID, mTargetLanguage.getId());
} else {
outState.remove(STATE_TARGET_LANGUAGE_ID);
}
if (mUsfm != null) { //save state and make sure it's not running
outState.putString(STATE_USFM, mUsfm.toJson().toString());
mUsfm.setUpdateStatusListener(null);
mUsfm.setCancel(true);
if((currentState == eImportState.processingFiles) // if doing initial processing, we clean up and start over
|| (currentState == eImportState.finished)) { // if finished we cleanup
mUsfm.cleanup();
}
} else {
outState.remove(STATE_USFM);
}
if (mCount != null) {
outState.putInt(STATE_PROMPT_NAME_COUNTER, mCount.counter);
} else {
outState.remove(STATE_PROMPT_NAME_COUNTER);
}
outState.putBoolean(STATE_FINISH_SUCCESS, mFinishedSuccess);
super.onSaveInstanceState(outState);
}
@Override
public void onItemClick(TargetLanguage targetLanguage) {
mTargetLanguage = targetLanguage;
if (null != targetLanguage) {
getFragmentManager().beginTransaction().remove((TargetLanguageListFragment) mFragment).commit();
mFragment = null;
processUsfmFile();
} else {
cancelled();
}
}
@Override
public void onItemClick(String projectId) {
setBook(projectId);
}
/**
* use the project ID
*
* @param projectId
*/
private void setBook(String projectId) {
if (projectId != null) {
getFragmentManager().beginTransaction().remove((ProjectListFragment) mFragment).commit();
mFragment = null;
mProgressDialog.show();
final MissingNameItem item = mMissingNameItems[mCount.counter];
usfmProcessBook(item, projectId);
} else { //book cancelled
usfmPromptForNextName();
}
}
/**
* user cancelled import
*/
private void cancelled() {
cleanupUsfmImport();
Intent data = new Intent();
setResult(RESULT_CANCELED, data);
finish();
}
/**
* user completed import
*/
private void finished() {
cleanupUsfmImport();
Intent data = new Intent();
setResult(RESULT_OK, data);
finish();
}
private void cleanupUsfmImport() {
if (mUsfm != null) {
mUsfm.cleanup();
mUsfm = null;
}
if(mProgressDialog != null) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
public interface OnFinishedListener {
void onFinished(boolean success);
}
public interface OnPromptFinishedListener {
void onFinished(boolean success, String name);
}
/**
* enum that keeps track of current state of USFM import
*/
public enum eImportState {
needLanguage(0),
processingFiles(1),
promptingForBookName(2),
showingProcessingResults(3),
importingFiles(4),
showingImportResults(5),
finished(6);
private int _value;
eImportState(int Value) {
this._value = Value;
}
public int getValue() {
return _value;
}
public static eImportState fromInt(int i) {
for (eImportState b : eImportState.values()) {
if (b.getValue() == i) {
return b;
}
}
return null;
}
}
}