package de.blau.android.tasks; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Set; import java.util.concurrent.ExecutionException; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import com.drew.lang.annotations.NotNull; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.util.Log; import de.blau.android.App; import de.blau.android.ErrorCodes; import de.blau.android.Main; import de.blau.android.PostAsyncActionHandler; import de.blau.android.R; import de.blau.android.UploadResult; import de.blau.android.dialogs.ErrorAlert; import de.blau.android.dialogs.ForbiddenLogin; import de.blau.android.dialogs.InvalidLogin; import de.blau.android.dialogs.Progress; import de.blau.android.exception.OsmException; import de.blau.android.osm.BoundingBox; import de.blau.android.osm.Server; import de.blau.android.prefs.Preferences; import de.blau.android.util.FileUtil; import de.blau.android.util.IssueAlert; import de.blau.android.util.SavingHelper; import de.blau.android.util.Snack; public class TransferTasks { private static final String DEBUG_TAG = TransferTasks.class.getSimpleName(); /** Maximum closed age to display: 7 days. */ private static final long MAX_CLOSED_AGE = 7 * 24 * 60 * 60 * 1000; /** viewbox needs to be less wide than this for displaying bugs, just to avoid querying the whole world for bugs */ private static final int TOLERANCE_MIN_VIEWBOX_WIDTH = 40000 * 32; /** * Download tasks for a bounding box, actual requests will depend on what the current filter for tasks is set to * * @param context Android context * @param server current server configuration * @param box the bounding box * @param add if true merge teh download with existing task data * @param handler handler to run after the download if not null */ static public void downloadBox(@NotNull final Context context, @NotNull final Server server, @NotNull final BoundingBox box, final boolean add, @Nullable final PostAsyncActionHandler handler) { final TaskStorage bugs = App.getTaskStorage(); final Preferences prefs = new Preferences(context); try { box.makeValidForApi(); } catch (OsmException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } // TODO remove this? and replace with better error messaging new AsyncTask<Void, Void, Collection<Task>>() { @Override protected Collection<Task> doInBackground(Void... params) { Log.d(DEBUG_TAG,"querying server for " + box); Set<String> bugFilter = prefs.taskFilter(); Collection<Task> result = new ArrayList<Task>(); Collection<Note> noteResult = null; Resources r = context.getResources(); if (bugFilter.contains(r.getString(R.string.bugfilter_notes))) { noteResult = server.getNotesForBox(box,1000); } if (noteResult != null) { result.addAll(noteResult); } Collection<OsmoseBug> osmoseResult = null; if (bugFilter.contains(r.getString(R.string.bugfilter_osmose_error)) || bugFilter.contains(r.getString(R.string.bugfilter_osmose_warning)) || bugFilter.contains(r.getString(R.string.bugfilter_osmose_minor_issue))) { osmoseResult = OsmoseServer.getBugsForBox(context, box, 1000); } if (osmoseResult != null) { result.addAll(osmoseResult); } return result; } @Override protected void onPostExecute(Collection<Task> result) { if (result == null) { Log.d(DEBUG_TAG,"no bugs found"); return; } if (!add) { Log.d(DEBUG_TAG,"resetting bug storage"); bugs.reset(); } bugs.add(box); long now = System.currentTimeMillis(); for (Task b : result) { Log.d(DEBUG_TAG,"got bug " + b.getDescription() + " " + bugs.toString()); // add open bugs or closed bugs younger than 7 days if (!bugs.contains(b) && (!b.isClosed() || (now - b.getLastUpdate().getTime()) < MAX_CLOSED_AGE)) { bugs.add(b); Log.d(DEBUG_TAG,"adding bug " + b.getDescription()); if (!b.isClosed()) { IssueAlert.alert(context, b); } } } if (handler != null) { handler.onSuccess(); } } }.execute(); } /** * Upload Notes or bugs to server, needs to be called from main for now (mainly for OAuth dependency) * * @param main instance of main calling this * @param server current server config * @param postUploadHandler execute code after an upload */ static public void upload(@NotNull final Main main, final Server server, @Nullable final PostAsyncActionHandler postUploadHandler) { final String PROGRESS_TAG = "tasks"; if (server != null) { final ArrayList<Task>queryResult = App.getTaskStorage().getTasks(); // check if we need to oAuth first for (Task b:queryResult) { if (b.changed && b instanceof Note) { if (server.isLoginSet()) { if (server.needOAuthHandshake()) { main.oAuthHandshake(server, new PostAsyncActionHandler() { @Override public void onSuccess() { Preferences prefs = new Preferences(main); upload(main, prefs.getServer(), postUploadHandler); } @Override public void onError() { } }); if (server.getOAuth()) { // if still set Snack.barError(main, R.string.toast_oauth); } return; } } else { ErrorAlert.showDialog(main,ErrorCodes.NO_LOGIN_DATA); return; } } } new AsyncTask<Void, Void, Boolean>() { @Override protected void onPreExecute() { Progress.showDialog(main, Progress.PROGRESS_UPLOADING, PROGRESS_TAG); Log.d(DEBUG_TAG,"starting up load of total " + queryResult.size() + " tasks"); } @Override protected Boolean doInBackground(Void... params) { boolean uploadFailed = false; for (Task b:queryResult) { if (b.changed) { try { Thread.sleep(100); // attempt at workaround of Osmose issues } catch (InterruptedException e) {} Log.d(DEBUG_TAG, b.getDescription()); if (b instanceof Note) { Note n = (Note)b; NoteComment nc = n.getLastComment(); if (nc != null && nc.isNew()) { uploadFailed = !uploadNote(main, server, n, nc.getText(), n.isClosed(), true, null) || uploadFailed; } else { uploadFailed = !uploadNote(main, server, n, null, n.isClosed(), true, null) || uploadFailed; // just a state change } } else if (b instanceof OsmoseBug) { uploadFailed = uploadOsmoseBug(main, (OsmoseBug)b, null) || uploadFailed; } } } return uploadFailed; } @Override protected void onPostExecute(Boolean uploadFailed) { Progress.dismissDialog(main, Progress.PROGRESS_UPLOADING, PROGRESS_TAG); if (!uploadFailed) { if (postUploadHandler != null) { postUploadHandler.onSuccess(); } Snack.barInfo(main, R.string.openstreetbug_commit_ok); } else { if (postUploadHandler != null) { postUploadHandler.onError(); } Snack.barError(main, R.string.openstreetbug_commit_fail); } } }.execute(); } } /** * Upload single bug state * * @param context the Android context * @param b osmose bug to upload * @param postUploadHandler if not null run this handler after upload * @return true if successful */ @SuppressLint("InlinedApi") static public boolean uploadOsmoseBug(@NotNull final Context context, @NotNull final OsmoseBug b, @Nullable final PostAsyncActionHandler postUploadHandler) { Log.d(DEBUG_TAG, "uploadOsmoseBug"); AsyncTask<Void, Void, Boolean> a = new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(Void... params) { return OsmoseServer.changeState(context, (OsmoseBug)b); } @Override protected void onPostExecute(Boolean uploadFailed) { if (!uploadFailed) { if (postUploadHandler != null) { postUploadHandler.onSuccess(); } } else { if (postUploadHandler != null) { postUploadHandler.onError(); } } } }; if(Build.VERSION.SDK_INT >= 11) { a.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { a.execute(); } try { return a.get(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } return false; } /** * Commit changes to a Note * * @param activity activity that called this * @param server Server configuration * @param note the Note to upload * @param comment Comment to add to the Note. * @param close if true the Note is to be closed. * @param quiet don't display an error message on errors * @param postUploadHandler execute code after an upload * @return true if upload was successful */ @TargetApi(11) static public boolean uploadNote(@NonNull final FragmentActivity activity, @NonNull final Server server, @NonNull final Note note, final String comment, final boolean close, final boolean quiet, @Nullable final PostAsyncActionHandler postUploadHandler) { Log.d(DEBUG_TAG, "uploadNote"); if (server != null) { if (server.isLoginSet()) { if (server.needOAuthHandshake()) { if (activity instanceof Main) { ((Main)activity).oAuthHandshake(server, new PostAsyncActionHandler() { @Override public void onSuccess() { Preferences prefs = new Preferences(activity); uploadNote(activity, prefs.getServer(), note, comment, close, quiet, postUploadHandler); } @Override public void onError() { } }); } if (server.getOAuth()) { // if still set Snack.barError(activity, R.string.toast_oauth); } return false; } } else { ErrorAlert.showDialog(activity,ErrorCodes.NO_LOGIN_DATA); return false; } CommitTask ct = new CommitTask(note, comment, close) { /** Flag to track if the bug is new. */ private boolean newBug; @Override protected void onPreExecute() { Log.d(DEBUG_TAG,"onPreExecute"); newBug = bug.isNew(); if (!quiet) { Progress.showDialog(activity, Progress.PROGRESS_UPLOADING); } } @Override protected UploadResult doInBackground(Server... args) { // execute() is called below with no arguments (args will be empty) // getDisplayName() is deferred to here in case a lengthy OSM query // is required to determine the nickname Log.d(DEBUG_TAG,"doInBackground " + server.getReadWriteUrl()); return super.doInBackground(server); } @Override protected void onPostExecute(UploadResult result) { Log.d(DEBUG_TAG,"onPostExecute"); if (newBug && !App.getTaskStorage().contains(bug)) { App.getTaskStorage().add(bug); } if (result.error == ErrorCodes.OK) { // upload successful bug.changed = false; if (postUploadHandler != null) { postUploadHandler.onSuccess(); } } if (!quiet) { Progress.dismissDialog(activity, Progress.PROGRESS_UPLOADING); // Toast.makeText(context, result ? R.string.openstreetbug_commit_ok : R.string.openstreetbug_commit_fail, Toast.LENGTH_SHORT).show(); if (!activity.isFinishing()) { if (result.error == ErrorCodes.INVALID_LOGIN) { InvalidLogin.showDialog(activity); } else if (result.error == ErrorCodes.FORBIDDEN) { ForbiddenLogin.showDialog(activity,result.message); } else if (result.error != 0) { ErrorAlert.showDialog(activity,result.error); } } } } }; // FIXME seems as if AsyncTask tends to run out of threads here .... not clear if executeOnExecutor actually helps if(Build.VERSION.SDK_INT >= 11) { ct.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { ct.execute(); } try { return ct.get().error == ErrorCodes.OK; } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } } return false; } /** * Write Notes to a file in (J)OSM compatible format * * If fileName contains directories these are created, otherwise it is stored in the standard public dir * * @param activity activity that called this * @param all if true write all notes, if false just those that have been modified * @param fileName file to write to */ static public void writeOsnFile(@NotNull final FragmentActivity activity, final boolean all, final String fileName) { new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_SAVING); } @Override protected Integer doInBackground(Void... arg) { final ArrayList<Task>queryResult = App.getTaskStorage().getTasks(); int result = 0; try { File outfile = new File(fileName); String parent = outfile.getParent(); if (parent == null) { // no directory specified, save to standard location outfile = new File(FileUtil.getPublicDirectory(), fileName); } else { // ensure directory exists File outdir = new File(parent); //noinspection ResultOfMethodCallIgnored outdir.mkdirs(); if (!outdir.isDirectory()) { throw new IOException("Unable to create directory " + outdir.getPath()); } } Log.d(DEBUG_TAG,"Saving to " + outfile.getPath()); final OutputStream out = new BufferedOutputStream(new FileOutputStream(outfile)); try { XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer(); serializer.setOutput(out, "UTF-8"); serializer.startDocument("UTF-8", null); serializer.startTag(null, "osm-notes"); for (Task t:queryResult) { if (t instanceof Note) { Note n = (Note) t; if (all || n.getId() < 0 || n.hasBeenChanged()) { n.toJosmXml(serializer); } } } serializer.endTag(null, "osm-notes"); serializer.endDocument(); } catch (IllegalArgumentException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } catch (IllegalStateException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } catch (XmlPullParserException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } finally { SavingHelper.close(out); } } catch (IOException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } return result; } @Override protected void onPostExecute(Integer result) { Progress.dismissDialog(activity, Progress.PROGRESS_SAVING); if (result != 0) { if (result == ErrorCodes.OUT_OF_MEMORY) { System.gc(); if (App.getTaskStorage().hasChanges()) { result = ErrorCodes.OUT_OF_MEMORY_DIRTY; } } if (!activity.isFinishing()) { ErrorAlert.showDialog(activity,result); } } } }.execute(); } }