package com.door43.translationstudio.newui; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentTransaction; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.content.FileProvider; import android.support.v4.provider.DocumentFile; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.Button; import com.door43.tools.reporting.Logger; import com.door43.translationstudio.AppContext; import com.door43.translationstudio.R; import com.door43.translationstudio.core.Project; import com.door43.translationstudio.core.TargetTranslation; import com.door43.translationstudio.core.TranslationViewMode; import com.door43.translationstudio.core.Translator; import com.door43.translationstudio.dialogs.CustomAlertDialog; import com.door43.translationstudio.newui.translate.TargetTranslationActivity; import com.door43.translationstudio.tasks.CreateRepositoryTask; import com.door43.translationstudio.tasks.PullTargetTranslationTask; import com.door43.translationstudio.tasks.PushTargetTranslationTask; import com.door43.translationstudio.tasks.RegisterSSHKeysTask; import com.door43.translationstudio.util.SdUtils; import com.door43.util.tasks.GenericTaskWatcher; import com.door43.util.tasks.ManagedTask; import com.door43.util.tasks.TaskManager; import com.door43.widget.ViewUtil; import org.apache.commons.io.IOUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.merge.MergeStrategy; import java.io.File; import java.io.OutputStream; import java.security.InvalidParameterException; import java.util.Map; /** * Created by joel on 10/5/2015. */ public class BackupDialog extends DialogFragment implements GenericTaskWatcher.OnFinishedListener { public static final String ARG_TARGET_TRANSLATION_ID = "target_translation_id"; public static final String TAG = "backup-dialog"; private static final String STATE_SETTING_DEVICE_ALIAS = "state_setting_device_alias"; private TargetTranslation targetTranslation; private GenericTaskWatcher taskWatcher; private boolean settingDeviceAlias = false; private Button mBackupToCloudButton = null; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); dialog.setCanceledOnTouchOutside(true); return dialog; } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); View v = inflater.inflate(R.layout.dialog_backup, container, false); // get target translation to backup Bundle args = getArguments(); if(args != null && args.containsKey(ARG_TARGET_TRANSLATION_ID)) { String targetTranslationId = args.getString(ARG_TARGET_TRANSLATION_ID, null); targetTranslation = AppContext.getTranslator().getTargetTranslation(targetTranslationId); if(targetTranslation == null) { throw new InvalidParameterException("The target translation '" + targetTranslationId + "' is invalid"); } } else { throw new InvalidParameterException("The target translation id was not specified"); } targetTranslation.setDefaultContributor(AppContext.getProfile().getNativeSpeaker()); mBackupToCloudButton = (Button)v.findViewById(R.id.backup_to_cloud); Button backupToSDButton = (Button)v.findViewById(R.id.backup_to_sd); Button backupToAppButton = (Button)v.findViewById(R.id.backup_to_app); Button backupToDeviceButton = (Button)v.findViewById(R.id.backup_to_device); final String filename = targetTranslation.getId() + "." + Translator.ARCHIVE_EXTENSION; taskWatcher = new GenericTaskWatcher(getActivity(), R.string.backup); taskWatcher.setOnFinishedListener(this); if(savedInstanceState != null) { // check if returning from device alias dialog settingDeviceAlias = savedInstanceState.getBoolean(STATE_SETTING_DEVICE_ALIAS, false); } Button dismissButton = (Button)v.findViewById(R.id.dismiss_button); dismissButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); } }); backupToDeviceButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO: 11/18/2015 eventually we need to support bluetooth as well as an adhoc network if(AppContext.context().isNetworkAvailable()) { if(AppContext.getDeviceNetworkAlias() == null) { // get device alias FragmentTransaction ft = getFragmentManager().beginTransaction(); Fragment prev = getFragmentManager().findFragmentByTag(BackupDialog.TAG); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); settingDeviceAlias = true; DeviceNetworkAliasDialog dialog = new DeviceNetworkAliasDialog(); dialog.show(ft, BackupDialog.TAG); } else { showP2PDialog(); } } else { Snackbar snack = Snackbar.make(getActivity().findViewById(android.R.id.content), R.string.internet_not_available, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, getResources().getColor(R.color.light_primary_text)); snack.show(); } } }); // backup buttons mBackupToCloudButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(AppContext.context().isNetworkAvailable()) { // make sure we have a gogs user if(AppContext.getProfile().gogsUser == null) { FragmentTransaction ft = getFragmentManager().beginTransaction(); Door43LoginDialog dialog = new Door43LoginDialog(); dialog.show(ft, Door43LoginDialog.TAG); return; } PullTargetTranslationTask task = new PullTargetTranslationTask(targetTranslation); taskWatcher.watch(task); TaskManager.addTask(task, PullTargetTranslationTask.TASK_ID); } else { Snackbar snack = Snackbar.make(getActivity().findViewById(android.R.id.content), R.string.internet_not_available, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, getResources().getColor(R.color.light_primary_text)); snack.show(); } } }); backupToSDButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (SdUtils.doWeNeedToRequestSdCardAccess()) { final CustomAlertDialog dialog = CustomAlertDialog.Create(getActivity()); dialog.setTitle(R.string.enable_sd_card_access_title) .setMessageHtml(R.string.enable_sd_card_access) .setPositiveButton(R.string.confirm, new View.OnClickListener() { @Override public void onClick(View v) { SdUtils.triggerStorageAccessFramework(getActivity()); } }) .setNegativeButton(R.string.label_skip, new View.OnClickListener() { @Override public void onClick(View v) { doSdCardBackup(filename); } }) .show("approve-SD-access"); } else { doSdCardBackup(filename); } } }); backupToAppButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { File exportFile = new File(AppContext.getSharingDir(), filename); try { AppContext.getTranslator().exportArchive(targetTranslation, exportFile); } catch (Exception e) { Logger.e(BackupDialog.class.getName(), "Failed to export the target translation " + targetTranslation.getId(), e); } if (exportFile.exists()) { Uri u = FileProvider.getUriForFile(getActivity(), "com.door43.translationstudio.fileprovider", exportFile); Intent i = new Intent(Intent.ACTION_SEND); i.setType("application/zip"); i.putExtra(Intent.EXTRA_STREAM, u); startActivity(Intent.createChooser(i, "Email:")); } else { Snackbar snack = Snackbar.make(getActivity().findViewById(android.R.id.content), R.string.translation_export_failed, Snackbar.LENGTH_LONG); ViewUtil.setSnackBarTextColor(snack, getResources().getColor(R.color.light_primary_text)); snack.show(); } } }); // connect to existing tasks PullTargetTranslationTask pullTask = (PullTargetTranslationTask)TaskManager.getTask(PullTargetTranslationTask.TASK_ID); RegisterSSHKeysTask keysTask = (RegisterSSHKeysTask)TaskManager.getTask(RegisterSSHKeysTask.TASK_ID); CreateRepositoryTask repoTask = (CreateRepositoryTask)TaskManager.getTask(CreateRepositoryTask.TASK_ID); PushTargetTranslationTask pushTask = (PushTargetTranslationTask)TaskManager.getTask(PushTargetTranslationTask.TASK_ID); if(pullTask != null) { taskWatcher.watch(pullTask); } else if (keysTask != null) { taskWatcher.watch(keysTask); } else if(repoTask != null) { taskWatcher.watch(repoTask); } else if(pushTask != null) { taskWatcher.watch(pushTask); } // attach to dialogs MergeConflictsDialog mergeConflictsDialog = (MergeConflictsDialog)getFragmentManager().findFragmentByTag(MergeConflictsDialog.TAG); if(mergeConflictsDialog != null) { attachMergeConflictListener(mergeConflictsDialog); } return v; } /** * back up project - will try to write to SD card if available - otherwise will save to internal memory * @param filename */ private void doSdCardBackup(String filename) { // TODO: 10/27/2015 have the user choose the file location String fileName = System.currentTimeMillis() / 1000L + "_" + filename; boolean success = false; boolean canWriteToSdCardBackupLollipop = false; DocumentFile baseFolder = null; String filePath = null; DocumentFile sdCardFile = null; OutputStream out = null; try { if(SdUtils.isSdCardPresentLollipop()) { baseFolder = SdUtils.sdCardMkdirs(SdUtils.DOWNLOAD_TRANSLATION_STUDIO_FOLDER); canWriteToSdCardBackupLollipop = baseFolder != null; } if (canWriteToSdCardBackupLollipop) { // default to writing to SD card if available filePath = SdUtils.getPathString(baseFolder); if (baseFolder.canWrite()) { sdCardFile = baseFolder.createFile("image", fileName); filePath = SdUtils.getPathString(sdCardFile); out = AppContext.context().getContentResolver().openOutputStream(sdCardFile.getUri()); AppContext.getTranslator().exportArchive(targetTranslation, out, fileName); success = true; } } else { File exportFile = new File(AppContext.getPublicDownloadsDirectory(), fileName); filePath = exportFile.toString(); AppContext.getTranslator().exportArchive(targetTranslation, exportFile); success = exportFile.exists(); } } catch (Exception e) { success = false; if(sdCardFile != null) { try { if(null != out) { IOUtils.closeQuietly(out); } sdCardFile.delete(); } catch(Exception e2) { } } Logger.e(BackupDialog.class.getName(), "Failed to export the target translation " + targetTranslation.getId(), e); } if (success) { showBackupResults(R.string.backup_success, filePath); } else { showBackupResults(R.string.backup_failed, filePath); } } private void showBackupResults(final int textResId, final String filePath) { String message = getResources().getString(textResId); if(filePath != null) { message += "\n" + filePath; } CustomAlertDialog.Create(getActivity()) .setTitle(R.string.backup_to_sd) .setMessage(message) .setNeutralButton(R.string.dismiss, null) .show("Backup"); } @Override public void onResume() { if(settingDeviceAlias && AppContext.getDeviceNetworkAlias() != null) { settingDeviceAlias = false; showP2PDialog(); } super.onResume(); } /** * Displays the dialog for p2p sharing */ private void showP2PDialog() { FragmentTransaction ft = getFragmentManager().beginTransaction(); Fragment prev = getFragmentManager().findFragmentByTag(BackupDialog.TAG); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); ShareWithPeerDialog dialog = new ShareWithPeerDialog(); Bundle args = new Bundle(); args.putInt(ShareWithPeerDialog.ARG_OPERATION_MODE, ShareWithPeerDialog.MODE_SERVER); args.putString(ShareWithPeerDialog.ARG_TARGET_TRANSLATION, targetTranslation.getId()); args.putString(ShareWithPeerDialog.ARG_DEVICE_ALIAS, AppContext.getDeviceNetworkAlias()); dialog.setArguments(args); dialog.show(ft, BackupDialog.TAG); } @Override public void onFinished(ManagedTask task) { taskWatcher.stop(); if(task instanceof PullTargetTranslationTask) { PullTargetTranslationTask.Status status = ((PullTargetTranslationTask)task).getStatus(); // TRICKY: we continue to push for unknown status in case the repo was just created (the missing branch is an error) // the pull task will catch any errors if(status == PullTargetTranslationTask.Status.UP_TO_DATE || status == PullTargetTranslationTask.Status.UNKNOWN) { Logger.i(this.getClass().getName(), "Changes on the server were synced with " + targetTranslation.getId()); PushTargetTranslationTask pushtask = new PushTargetTranslationTask(targetTranslation, false); taskWatcher.watch(pushtask); TaskManager.addTask(pushtask, PushTargetTranslationTask.TASK_ID); } else if(status == PullTargetTranslationTask.Status.AUTH_FAILURE) { Logger.i(this.getClass().getName(), "Authentication failed"); // if we have already tried ask the user if they would like to try again if(AppContext.context().hasSSHKeys()) { showAuthFailure(); return; } RegisterSSHKeysTask keyTask = new RegisterSSHKeysTask(false); taskWatcher.watch(keyTask); TaskManager.addTask(keyTask, RegisterSSHKeysTask.TASK_ID); } else if(status == PullTargetTranslationTask.Status.NO_REMOTE_REPO) { Logger.i(this.getClass().getName(), "The repository " + targetTranslation.getId() + " could not be found"); // create missing repo CreateRepositoryTask repoTask = new CreateRepositoryTask(targetTranslation); taskWatcher.watch(repoTask); TaskManager.addTask(repoTask, CreateRepositoryTask.TASK_ID); } else if(status == PullTargetTranslationTask.Status.MERGE_CONFLICTS) { Logger.i(this.getClass().getName(), "The server contains conflicting changes for " + targetTranslation.getId()); notifyMergeConflicts(((PullTargetTranslationTask)task).getConflicts()); } else { notifyBackupFailed(targetTranslation); } } else if(task instanceof RegisterSSHKeysTask) { if(((RegisterSSHKeysTask)task).isSuccess()) { Logger.i(this.getClass().getName(), "SSH keys were registered with the server"); // try to push again PullTargetTranslationTask pullTask = new PullTargetTranslationTask(targetTranslation); taskWatcher.watch(pullTask); TaskManager.addTask(pullTask, PullTargetTranslationTask.TASK_ID); } else { notifyBackupFailed(targetTranslation); } } else if(task instanceof CreateRepositoryTask) { if(((CreateRepositoryTask)task).isSuccess()) { Logger.i(this.getClass().getName(), "A new repository " + targetTranslation.getId() + " was created on the server"); PullTargetTranslationTask pullTask = new PullTargetTranslationTask(targetTranslation); taskWatcher.watch(pullTask); TaskManager.addTask(pullTask, PullTargetTranslationTask.TASK_ID); } else { notifyBackupFailed(targetTranslation); } } else if(task instanceof PushTargetTranslationTask) { PushTargetTranslationTask.Status status =((PushTargetTranslationTask)task).getStatus(); final String message = ((PushTargetTranslationTask)task).getMessage(); if(status == PushTargetTranslationTask.Status.OK) { Logger.i(this.getClass().getName(), "The target translation " + targetTranslation.getId() + " was pushed to the server"); CustomAlertDialog.Create(getActivity()) .setTitle(R.string.success).setMessage(R.string.project_uploaded).setPositiveButton(R.string.dismiss, null) .setNeutralButton(R.string.label_details, new View.OnClickListener() { @Override public void onClick(View v) { CustomAlertDialog.Create(getActivity()) .setTitle(R.string.project_uploaded).setMessage(message).setPositiveButton(R.string.dismiss, null).show("PubDetails"); } }).show("backup-finished"); } else if(status == PushTargetTranslationTask.Status.AUTH_FAILURE) { Logger.i(this.getClass().getName(), "Authentication failed"); showAuthFailure(); } else { notifyBackupFailed(targetTranslation); } } } private void notifyMergeConflicts(Map<String, int[][]> conflicts) { FragmentTransaction ft = getFragmentManager().beginTransaction(); MergeConflictsDialog dialog = new MergeConflictsDialog(); attachMergeConflictListener(dialog); dialog.show(ft, MergeConflictsDialog.TAG); } private void attachMergeConflictListener(MergeConflictsDialog dialog) { dialog.setOnClickListener(new MergeConflictsDialog.OnClickListener() { @Override public void onReview() { if(getActivity() instanceof TargetTranslationActivity) { ((TargetTranslationActivity) getActivity()).notifyDatasetChanged(); BackupDialog.this.dismiss(); // TODO: 4/20/16 it woulid be nice to navigate directly to the first conflict } else { // ask parent activity to navigate to a new activity Intent intent = new Intent(getActivity(), TargetTranslationActivity.class); Bundle args = new Bundle(); args.putString(AppContext.EXTRA_TARGET_TRANSLATION_ID, targetTranslation.getId()); // TODO: 4/20/16 it woulid be nice to navigate directly to the first conflict // args.putString(AppContext.EXTRA_CHAPTER_ID, chapterId); // args.putString(AppContext.EXTRA_FRAME_ID, frameId); args.putString(AppContext.EXTRA_VIEW_MODE, TranslationViewMode.REVIEW.toString()); intent.putExtras(args); startActivity(intent); getActivity().finish(); } } @Override public void onKeepServer() { try { Git git = targetTranslation.getRepo().getGit(); ResetCommand resetCommand = git.reset(); resetCommand.setMode(ResetCommand.ResetType.HARD) .setRef("backup-master") .call(); // try to pull again PullTargetTranslationTask pullTask = new PullTargetTranslationTask(targetTranslation, MergeStrategy.THEIRS); taskWatcher.watch(pullTask); TaskManager.addTask(pullTask, PullTargetTranslationTask.TASK_ID); } catch (Exception e) { Logger.e(this.getClass().getName(), "Failed to keep server changes durring publish", e); notifyBackupFailed(targetTranslation); } } @Override public void onKeepLocal() { try { Git git = targetTranslation.getRepo().getGit(); ResetCommand resetCommand = git.reset(); resetCommand.setMode(ResetCommand.ResetType.HARD) .setRef("backup-master") .call(); // try to pull again PullTargetTranslationTask pullTask = new PullTargetTranslationTask(targetTranslation, MergeStrategy.OURS); taskWatcher.watch(pullTask); TaskManager.addTask(pullTask, PullTargetTranslationTask.TASK_ID); } catch (Exception e) { Logger.e(this.getClass().getName(), "Failed to keep local changes durring publish", e); notifyBackupFailed(targetTranslation); } } @Override public void onCancel() { try { Git git = targetTranslation.getRepo().getGit(); ResetCommand resetCommand = git.reset(); resetCommand.setMode(ResetCommand.ResetType.HARD) .setRef("backup-master") .call(); } catch (Exception e) { Logger.e(this.getClass().getName(), "Failed to restore local changes", e); } // TODO: 4/20/16 notify canceled } }); } /** * Displays a dialog to the user indicating the publish failed. * Includes an option to submit a bug report * @param targetTranslation */ private void notifyBackupFailed(final TargetTranslation targetTranslation) { final Project project = AppContext.getLibrary().getProject(targetTranslation.getProjectId(), "en"); CustomAlertDialog.Create(getActivity()) .setTitle(R.string.publish) .setMessage(R.string.upload_failed) .setPositiveButton(R.string.dismiss, null) .setNeutralButton(R.string.menu_bug, new View.OnClickListener() { @Override public void onClick(View v) { // open bug report dialog FragmentTransaction ft = getFragmentManager().beginTransaction(); Fragment prev = getFragmentManager().findFragmentByTag("bugDialog"); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); FeedbackDialog dialog = new FeedbackDialog(); Bundle args = new Bundle(); String message = "Failed to backup the translation of " + project.name + " into " + targetTranslation.getTargetLanguageName() + ".\ntargetTranslation: " + targetTranslation.getId() + "\n--------\n\n"; args.putString(FeedbackDialog.ARG_MESSAGE, message); dialog.setArguments(args); dialog.show(ft, "bugDialog"); } }).show("backup-failed"); } public void showAuthFailure() { CustomAlertDialog.Create(getActivity()) .setTitle(R.string.upload_failed).setMessage(R.string.auth_failure_retry) .setPositiveButton(R.string.yes, new View.OnClickListener() { @Override public void onClick(View v) { RegisterSSHKeysTask keyTask = new RegisterSSHKeysTask(true); taskWatcher.watch(keyTask); TaskManager.addTask(keyTask, RegisterSSHKeysTask.TASK_ID); } }) .setNegativeButton(R.string.no, new View.OnClickListener() { @Override public void onClick(View v) { notifyBackupFailed(targetTranslation); } }) .show("auth-failed"); } @Override public void onSaveInstanceState(Bundle out) { // remember if the device alias dialog is open out.putBoolean(STATE_SETTING_DEVICE_ALIAS, settingDeviceAlias); super.onSaveInstanceState(out); } @Override public void onDestroy() { if(taskWatcher != null) { taskWatcher.stop(); } super.onDestroy(); } }