package de.blau.android.javascript;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Map;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import com.drew.lang.annotations.NotNull;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.PopupMenu.OnMenuItemClickListener;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import de.blau.android.App;
import de.blau.android.ErrorCodes;
import de.blau.android.Logic;
import de.blau.android.PostAsyncActionHandler;
import de.blau.android.R;
import de.blau.android.contract.Paths;
import de.blau.android.dialogs.Progress;
import de.blau.android.dialogs.ProgressDialog;
import de.blau.android.prefs.Preferences;
import de.blau.android.util.FileUtil;
import de.blau.android.util.ReadFile;
import de.blau.android.util.SaveFile;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.SelectFile;
import de.blau.android.util.Snack;
import de.blau.android.util.ThemeUtils;
/**
* Various JS related utility methods
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scopes_and_Contexts">https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scopes_and_Contexts</a>
* @see <a href="https://dxr.mozilla.org/mozilla/source/js/rhino/examples/DynamicScopes.java">https://dxr.mozilla.org/mozilla/source/js/rhino/examples/DynamicScopes.java</a>
* @author simon
*
*/
public class Utils {
private static final String DEBUG_TAG = "javascript.Utils";
/**
* Evaluate JS
* @param ctx android context
* @param scriptName name for error reporting
* @param script the javascript
* @return whatever the JS returned as a string
*/
@Nullable
public static String evalString(Context ctx, String scriptName, String script) {
Log.d(DEBUG_TAG, "Eval " + script);
org.mozilla.javascript.Context rhinoContext = App.getRhinoHelper(ctx).enterContext();
try {
Scriptable restrictedScope = App.getRestrictedRhinoScope(ctx);
Scriptable scope = rhinoContext.newObject(restrictedScope);
scope.setPrototype(restrictedScope);
scope.setParentScope(null);
Object result = rhinoContext.evaluateString(scope, script, scriptName, 1, null);
return org.mozilla.javascript.Context.toString(result);
} finally {
org.mozilla.javascript.Context.exit();
}
}
/**
* Evaluate JS associated with a key in a preset
* @param ctx android context
* @param scriptName name for error reporting
* @param script the javascript
* @param originalTags original tags the property editor was called with
* @param tags the current tags
* @param value any value associated with the key
* @return the value that should be assigned to the tag or null if no value should be set
*/
@Nullable
public static String evalString(Context ctx, String scriptName, String script, Map<String, ArrayList<String>> originalTags, Map<String, ArrayList<String>> tags, String value) {
org.mozilla.javascript.Context rhinoContext = App.getRhinoHelper(ctx).enterContext();
try {
Scriptable restrictedScope = App.getRestrictedRhinoScope(ctx);
Scriptable scope = rhinoContext.newObject(restrictedScope);
scope.setPrototype(restrictedScope);
scope.setParentScope(null);
Object wrappedOut = org.mozilla.javascript.Context.javaToJS(originalTags, scope);
ScriptableObject.putProperty(scope, "originalTags", wrappedOut);
wrappedOut = org.mozilla.javascript.Context.javaToJS(tags, scope);
ScriptableObject.putProperty(scope, "tags", wrappedOut);
wrappedOut = org.mozilla.javascript.Context.javaToJS(value, scope);
ScriptableObject.putProperty(scope, "value", wrappedOut);
Log.d(DEBUG_TAG, "Eval (preset): " + script);
Object result = rhinoContext.evaluateString(scope, script, scriptName, 1, null);
if (result==null) {
return null;
} else {
return org.mozilla.javascript.Context.toString(result);
}
} finally {
org.mozilla.javascript.Context.exit();
}
}
/**
* Evaluate a script making the current logic available, this essentially allows access to all data
* @param ctx android context
* @param scriptName name for error reporting
* @param script the javascript
* @param logic an instance of Logic
* @return result of evaluating the JS as a string
*/
@Nullable
public static String evalString(Context ctx, String scriptName, String script, Logic logic) {
org.mozilla.javascript.Context rhinoContext = App.getRhinoHelper(ctx).enterContext();
try {
Scriptable restrictedScope = App.getRestrictedRhinoScope(ctx);
Scriptable scope = rhinoContext.newObject(restrictedScope);
scope.setPrototype(restrictedScope);
scope.setParentScope(null);
Object wrappedOut = org.mozilla.javascript.Context.javaToJS(logic, scope);
ScriptableObject.putProperty(scope, "logic", wrappedOut);
Log.d(DEBUG_TAG, "Eval (logic): " + script);
Object result = rhinoContext.evaluateString(scope, script, scriptName, 1, null);
if (result==null) {
return null;
} else {
return org.mozilla.javascript.Context.toString(result);
}
} finally {
org.mozilla.javascript.Context.exit();
}
}
/**
* Display a simple console with multi-line input and output from the eval method
* @param activity android context
* @param msgResource sub title to display
* @param callback callback that actually evaluates the input
*/
@SuppressLint("InflateParams")
public static void jsConsoleDialog(final FragmentActivity activity, int msgResource, final EvalCallback callback) {
// Create some useful objects
final LayoutInflater inflater = ThemeUtils.getLayoutInflater(activity);
final Preferences prefs = new Preferences(activity);
Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.tag_menu_js_console);
builder.setMessage(msgResource);
View v = inflater.inflate(R.layout.debug_js, null);
final EditText input = (EditText)v.findViewById(R.id.js_input);
final TextView output = (TextView)v.findViewById(R.id.js_output);
builder.setView(v);
builder.setPositiveButton(R.string.evaluate, null);
builder.setNegativeButton(R.string.dismiss, null);
builder.setNeutralButton(R.string.share, null);
AlertDialog dialog = builder.create();
final Handler handler = new Handler();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
Button positive = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
positive.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final AsyncTask<String, Void, String> runner = new AsyncTask<String, Void, String>() {
final AlertDialog progress = ProgressDialog.get(activity, Progress.PROGRESS_RUNNING);
@Override
protected void onPreExecute() {
progress.show();
}
@Override
protected String doInBackground(String... js) {
try {
return callback.eval(js[0]);
} catch (Exception ex) {
Log.e(DEBUG_TAG, "dialog failed with " + ex);
ex.printStackTrace();
return ex.getMessage();
}
}
@Override
protected void onPostExecute(final String result) {
try {
progress.dismiss();
} catch (Exception ex) {
Log.e(DEBUG_TAG, "dismiss dialog failed with " + ex);
}
output.setText(result);
}
};
runner.execute(input.getText().toString());
}
});
Button neutral = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL);
Drawable share = ThemeUtils.getTintedDrawable(activity, R.drawable.ic_more_vert_black_36dp, R.attr.colorAccent);
neutral.setCompoundDrawablesWithIntrinsicBounds(share, null, null, null);
neutral.setText("");
neutral.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
PopupMenu popupMenu = new PopupMenu(activity, view);
popupMenu.inflate(R.menu.js_popup);
popupMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.js_menu_share:
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT,input.getText().toString());
sendIntent.setType("text/plain");
activity.startActivity(sendIntent);
break;
case R.id.js_menu_save:
if (prefs.getString(R.string.config_scriptsPreferredDir_key)==null) {
File scriptsDir;
try {
scriptsDir = FileUtil.getPublicDirectory(FileUtil.getPublicDirectory(),Paths.DIRECTORY_PATH_SCRIPTS);
} catch (IOException e) {
Snack.barError(activity, e.getMessage());
return false;
}
prefs.putString(R.string.config_scriptsPreferredDir_key, scriptsDir.getAbsolutePath());
}
SelectFile.save(activity, R.string.config_scriptsPreferredDir_key, new SaveFile(){
private static final long serialVersionUID = 1L;
@Override
public boolean save(Uri fileUri) {
writeScriptFile(activity, fileUri.getPath(), input.getText().toString(), null);
SelectFile.savePref(prefs, R.string.config_scriptsPreferredDir_key, fileUri);
return true;
}});
break;
case R.id.js_menu_read:
SelectFile.read(activity, R.string.config_scriptsPreferredDir_key, new ReadFile(){
private static final long serialVersionUID = 1L;
@Override
public boolean read(Uri fileUri) {
readScriptFile(activity, fileUri, input, null);
SelectFile.savePref(prefs, R.string.config_scriptsPreferredDir_key, fileUri);
return true;
}});
break;
}
return true;
}
});
popupMenu.show();
}
});
}
});
dialog.show();
}
/**
* Write data 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 fileName path of the file to save to
* @param postSaveHandler if not null executes code after saving
*/
private static void writeScriptFile(@NotNull final FragmentActivity activity, @NonNull final String fileName, @NonNull final String script, @Nullable final PostAsyncActionHandler postSaveHandler) {
new AsyncTask<Void, Void, Integer>() {
@Override
protected void onPreExecute() {
Progress.showDialog(activity, Progress.PROGRESS_SAVING);
}
@Override
protected Integer doInBackground(Void... arg) {
int result = 0;
FileOutputStream fout = null;
OutputStream out = null;
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());
fout = new FileOutputStream(outfile);
out = new BufferedOutputStream(fout);
try {
out.write(script.getBytes());
} 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 (IOException e) {
result = ErrorCodes.FILE_WRITE_FAILED;
Log.e(DEBUG_TAG, "Problem writing", e);
} finally {
SavingHelper.close(out);
SavingHelper.close(fout);
}
return result;
}
@Override
protected void onPostExecute(Integer result) {
Progress.dismissDialog(activity, Progress.PROGRESS_SAVING);
if (result != 0) {
if (postSaveHandler != null) {
postSaveHandler.onError();
}
} else {
if (postSaveHandler != null) {
postSaveHandler.onSuccess();
}
}
}
}.execute();
}
public static void readScriptFile(@NotNull final FragmentActivity activity, final Uri uri, final EditText input, final PostAsyncActionHandler postLoad) {
new AsyncTask<Void, Void, String>() {
@Override
protected void onPreExecute() {
Progress.showDialog(activity, Progress.PROGRESS_LOADING);
}
@Override
protected String doInBackground(Void... arg) {
InputStream is = null;
ByteArrayOutputStream result = null;
String r = null;
try {
if ("file".equals(uri.getScheme())) {
is = new FileInputStream(new File(uri.getPath()));
} else {
ContentResolver cr = activity.getContentResolver();
is = cr.openInputStream(uri);
}
result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
r = result.toString("UTF-8");
} catch (IOException e) {
Log.e(DEBUG_TAG, "Problem reading", e);
} finally {
SavingHelper.close(result);
SavingHelper.close(is);
}
return r;
}
@Override
protected void onPostExecute(String result) {
Progress.dismissDialog(activity, Progress.PROGRESS_LOADING);
if (result == null) {
if (postLoad != null) {
postLoad.onError();
}
} else {
if (postLoad != null) {
postLoad.onSuccess();
}
input.setText(result);
}
}
}.execute();
}
}