diff --git a/DroidFish/res/raw/about.html b/DroidFish/res/raw/about.html
index e4bb29c..5c31f74 100644
--- a/DroidFish/res/raw/about.html
+++ b/DroidFish/res/raw/about.html
@@ -220,6 +220,9 @@
Color picker, Copyright © 2010 Daniel Nilsson and Copyright © 2011 Sergey Margaritov.
+
+ Open exchange chess engine interface code by Gerhard Kalab, Apache 2.0 license.
+
Translations
diff --git a/DroidFish/src/com/kalab/chess/enginesupport/ChessEngine.java b/DroidFish/src/com/kalab/chess/enginesupport/ChessEngine.java
new file mode 100644
index 0000000..2aa899d
--- /dev/null
+++ b/DroidFish/src/com/kalab/chess/enginesupport/ChessEngine.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.kalab.chess.enginesupport;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+public class ChessEngine {
+
+ private static final String TAG = ChessEngine.class.getSimpleName();
+
+ private String name;
+ private String fileName;
+ private String authority;
+ private String packageName;
+
+ public ChessEngine(String name, String fileName, String authority, String packageName) {
+ this.name = name;
+ this.fileName = fileName;
+ this.authority = authority;
+ this.packageName = packageName;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getFileName() {
+ return this.fileName;
+ }
+
+ public Uri getUri() {
+ return Uri.parse("content://" + authority + "/" + fileName);
+ }
+
+ public File copyToFiles(ContentResolver contentResolver, File destination)
+ throws FileNotFoundException, IOException {
+ Uri uri = getUri();
+ File output = new File(destination, uri.getPath().toString());
+ copyUri(contentResolver, uri, output.getAbsolutePath());
+ return output;
+ }
+
+ public void copyUri(final ContentResolver contentResolver,
+ final Uri source, String targetFilePath) throws IOException,
+ FileNotFoundException {
+ InputStream istream = contentResolver.openInputStream(source);
+ copyFile(istream, targetFilePath);
+ setExecutablePermission(targetFilePath);
+ }
+
+ private void copyFile(InputStream istream, String targetFilePath)
+ throws FileNotFoundException, IOException {
+ FileOutputStream fout = new FileOutputStream(targetFilePath);
+ byte[] b = new byte[1024];
+ int numBytes = 0;
+ while ((numBytes = istream.read(b)) != -1) {
+ fout.write(b, 0, numBytes);
+ }
+ istream.close();
+ fout.close();
+ }
+
+ private void setExecutablePermission(String engineFileName) throws IOException {
+ String cmd[] = { "chmod", "744", engineFileName };
+ Process process = Runtime.getRuntime().exec(cmd);
+ try {
+ process.waitFor();
+ } catch (InterruptedException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+}
diff --git a/DroidFish/src/com/kalab/chess/enginesupport/ChessEngineResolver.java b/DroidFish/src/com/kalab/chess/enginesupport/ChessEngineResolver.java
new file mode 100644
index 0000000..5734c91
--- /dev/null
+++ b/DroidFish/src/com/kalab/chess/enginesupport/ChessEngineResolver.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.kalab.chess.enginesupport;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+public class ChessEngineResolver {
+
+ private static final String ENGINE_PROVIDER_MARKER = "intent.chess.provider.ENGINE";
+ private static final String TAG = ChessEngineResolver.class.getSimpleName();
+ private Context context;
+ private String target;
+
+ public ChessEngineResolver(Context context) {
+ super();
+ this.context = context;
+ this.target = Build.CPU_ABI;
+ }
+
+ public List resolveEngines() {
+ List result = new ArrayList();
+ final Intent intent = new Intent(ENGINE_PROVIDER_MARKER);
+ List list = context.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.GET_META_DATA);
+ for (ResolveInfo resolveInfo : list) {
+ String packageName = resolveInfo.activityInfo.packageName;
+ result = resolveEnginesForPackage(result, resolveInfo, packageName);
+ }
+ return result;
+ }
+
+ private List resolveEnginesForPackage(
+ List result, ResolveInfo resolveInfo,
+ String packageName) {
+ if (packageName != null) {
+ Log.d(TAG, "found engine provider, packageName=" + packageName);
+ Bundle bundle = resolveInfo.activityInfo.metaData;
+ if (bundle != null) {
+ String authority = bundle
+ .getString("chess.provider.engine.authority");
+ Log.d(TAG, "authority=" + authority);
+ if (authority != null) {
+ try {
+ Resources resources = context
+ .getPackageManager()
+ .getResourcesForApplication(
+ resolveInfo.activityInfo.applicationInfo);
+ int resId = resources.getIdentifier("enginelist",
+ "xml", packageName);
+ XmlResourceParser parser = resources.getXml(resId);
+ parseEngineListXml(parser, authority, result, packageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, e.getLocalizedMessage(), e);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ private void parseEngineListXml(XmlResourceParser parser, String authority,
+ List result, String packageName) {
+ try {
+ int eventType = parser.getEventType();
+ while (eventType != XmlResourceParser.END_DOCUMENT) {
+ String name = null;
+ try {
+ if (eventType == XmlResourceParser.START_TAG) {
+ name = parser.getName();
+ if (name.equalsIgnoreCase("engine")) {
+ String fileName = parser.getAttributeValue(null,
+ "filename");
+ String title = parser.getAttributeValue(null,
+ "name");
+ String targetSpecification = parser
+ .getAttributeValue(null, "target");
+ String[] targets = targetSpecification.split("\\|");
+ for (String cpuTarget : targets) {
+ if (target.equals(cpuTarget)) {
+ result.add(new ChessEngine(title, fileName,
+ authority, packageName));
+ }
+ }
+ }
+ }
+ eventType = parser.next();
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage(), e);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, e.getLocalizedMessage(), e);
+ }
+ }
+
+ /**
+ * Don't use this in production - this method is only for testing. Set the
+ * cpu target.
+ *
+ * @param target
+ * the cpu target to set
+ */
+ public void setTarget(String target) {
+ this.target = target;
+ }
+}
diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java
index 2cf65fd..3ad29f1 100644
--- a/DroidFish/src/org/petero/droidfish/DroidFish.java
+++ b/DroidFish/src/org/petero/droidfish/DroidFish.java
@@ -26,6 +26,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -57,6 +59,8 @@ import org.petero.droidfish.gamelogic.GameTree.Node;
import org.petero.droidfish.gamelogic.TimeControlData;
import org.petero.droidfish.gtb.Probe;
+import com.kalab.chess.enginesupport.ChessEngine;
+import com.kalab.chess.enginesupport.ChessEngineResolver;
import com.larvalabs.svgandroid.SVG;
import com.larvalabs.svgandroid.SVGParser;
@@ -479,6 +483,7 @@ public class DroidFish extends Activity implements GUIInterface {
new File(extDir + sep + pgnDir).mkdirs();
new File(extDir + sep + fenDir).mkdirs();
new File(extDir + sep + engineDir).mkdirs();
+ new File(extDir + sep + engineDir + sep + EngineUtil.openExchangeDir).mkdirs();
new File(extDir + sep + gtbDefaultDir).mkdirs();
new File(extDir + sep + rtbDefaultDir).mkdirs();
}
@@ -1154,21 +1159,29 @@ public class DroidFish extends Activity implements GUIInterface {
}
private final void setEngineTitle(String engine, int strength) {
- if (engine.contains("/")) {
- int idx = engine.lastIndexOf('/');
- String eName = engine.substring(idx + 1);
- engineTitleText.setText(eName);
- } else {
- String eName = getString(engine.equals("cuckoochess") ?
- R.string.cuckoochess_engine :
- R.string.stockfish_engine);
- boolean analysis = (ctrl != null) && ctrl.analysisMode();
- if ((strength < 1000) && !analysis) {
- engineTitleText.setText(String.format(Locale.US, "%s: %d%%", eName, strength / 10));
- } else {
- engineTitleText.setText(eName);
+ String eName = "";
+ if (EngineUtil.isOpenExchangeEngine(engine)) {
+ String engineFileName = new File(engine).getName();
+ ChessEngineResolver resolver = new ChessEngineResolver(this);
+ List engines = resolver.resolveEngines();
+ for (ChessEngine ce : engines) {
+ if (EngineUtil.openExchangeFileName(ce).equals(engineFileName)) {
+ eName = ce.getName();
+ break;
+ }
}
+ } else if (engine.contains("/")) {
+ int idx = engine.lastIndexOf('/');
+ eName = engine.substring(idx + 1);
+ } else {
+ eName = getString(engine.equals("cuckoochess") ?
+ R.string.cuckoochess_engine :
+ R.string.stockfish_engine);
+ boolean analysis = (ctrl != null) && ctrl.analysisMode();
+ if ((strength < 1000) && !analysis)
+ eName = String.format(Locale.US, "%s: %d%%", eName, strength / 10);
}
+ engineTitleText.setText(eName);
}
/** Update center field in second header line. */
@@ -2147,42 +2160,65 @@ public class DroidFish extends Activity implements GUIInterface {
}
private final Dialog selectEngineDialog(final boolean abortOnCancel) {
+ final ArrayList items = new ArrayList();
+ final ArrayList ids = new ArrayList();
+ ids.add("stockfish"); items.add(getString(R.string.stockfish_engine));
+ ids.add("cuckoochess"); items.add(getString(R.string.cuckoochess_engine));
+
+ final String sep = File.separator;
+ final String base = Environment.getExternalStorageDirectory() + sep + engineDir + sep;
+ {
+ ChessEngineResolver resolver = new ChessEngineResolver(this);
+ List engines = resolver.resolveEngines();
+ ArrayList> oexEngines = new ArrayList>();
+ for (ChessEngine engine : engines) {
+ if ((engine.getName() != null) && (engine.getFileName() != null) &&
+ (engine.getPackageName() != null)) {
+ oexEngines.add(new Pair(EngineUtil.openExchangeFileName(engine),
+ engine.getName()));
+ }
+ }
+ Collections.sort(oexEngines, new Comparator>() {
+ @Override
+ public int compare(Pair lhs, Pair rhs) {
+ return lhs.second.compareTo(rhs.second);
+ }
+ });
+ for (Pair eng : oexEngines) {
+ ids.add(base + EngineUtil.openExchangeDir + sep + eng.first);
+ items.add(eng.second);
+ }
+ }
+
String[] fileNames = findFilesInDirectory(engineDir, new FileNameFilter() {
@Override
public boolean accept(String filename) {
return !reservedEngineName(filename);
}
});
- final int numFiles = fileNames.length;
- final int nEngines = numFiles + 2;
- final String[] items = new String[nEngines];
- final String[] ids = new String[nEngines];
- int idx = 0;
- ids[idx] = "stockfish"; items[idx] = getString(R.string.stockfish_engine); idx++;
- ids[idx] = "cuckoochess"; items[idx] = getString(R.string.cuckoochess_engine); idx++;
- String sep = File.separator;
- String base = Environment.getExternalStorageDirectory() + sep + engineDir + sep;
- for (int i = 0; i < numFiles; i++) {
- ids[idx] = base + fileNames[i];
- items[idx] = fileNames[i];
- idx++;
+ for (String file : fileNames) {
+ ids.add(base + file);
+ items.add(file);
}
+
String currEngine = ctrl.getEngine();
int defaultItem = 0;
+ final int nEngines = items.size();
for (int i = 0; i < nEngines; i++) {
- if (ids[i].equals(currEngine)) {
+ if (ids.get(i).equals(currEngine)) {
defaultItem = i;
break;
}
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.select_chess_engine);
- builder.setSingleChoiceItems(items, defaultItem, new DialogInterface.OnClickListener() {
+ builder.setSingleChoiceItems(items.toArray(new String[0]), defaultItem,
+ new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
if ((item < 0) || (item >= nEngines))
return;
Editor editor = settings.edit();
- String engine = ids[item];
+ String engine = ids.get(item);
editor.putString("engine", engine);
editor.commit();
dialog.dismiss();
diff --git a/DroidFish/src/org/petero/droidfish/engine/EngineUtil.java b/DroidFish/src/org/petero/droidfish/engine/EngineUtil.java
index af011f4..df07948 100644
--- a/DroidFish/src/org/petero/droidfish/engine/EngineUtil.java
+++ b/DroidFish/src/org/petero/droidfish/engine/EngineUtil.java
@@ -18,11 +18,14 @@
package org.petero.droidfish.engine;
+import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import com.kalab.chess.enginesupport.ChessEngine;
+
import android.os.Build;
public class EngineUtil {
@@ -58,6 +61,43 @@ public class EngineUtil {
return netEngine;
}
+ public static final String openExchangeDir = "oex";
+
+ /** Return true if file "engine" is an open exchange engine. */
+ public static boolean isOpenExchangeEngine(String engine) {
+ File parent = new File(engine).getParentFile();
+ if (parent == null)
+ return false;
+ String parentDir = parent.getName();
+ return openExchangeDir.equals(parentDir);
+ }
+
+ /** Return a filename (without path) representing an open exchange engine. */
+ public static String openExchangeFileName(ChessEngine engine) {
+ String ret = "";
+ if (engine.getPackageName() != null)
+ ret += sanitizeString(engine.getPackageName());
+ ret += "-";
+ if (engine.getFileName() != null)
+ ret += sanitizeString(engine.getFileName());
+ return ret;
+ }
+
+ /** Remove characters from s that are not safe to use in a filename. */
+ private static String sanitizeString(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (((ch >= 'A') && (ch <= 'Z')) ||
+ ((ch >= 'a') && (ch <= 'z')) ||
+ ((ch >= '0') && (ch <= '9')))
+ sb.append(ch);
+ else
+ sb.append('_');
+ }
+ return sb.toString();
+ }
+
/** Executes chmod 744 exePath. */
final static native boolean chmod(String exePath);
diff --git a/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java b/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java
index aba4b53..44800d0 100644
--- a/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java
+++ b/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java
@@ -68,9 +68,11 @@ public class ExternalEngine extends UCIEngineBase {
@Override
protected void startProcess() {
try {
- String exePath = context.getFilesDir().getAbsolutePath() + "/engine.exe";
- copyFile(engineFileName, new File(exePath));
+ File exeDir = new File(context.getFilesDir(), "engine");
+ exeDir.mkdir();
+ String exePath = copyFile(engineFileName, exeDir);
chmod(exePath);
+ cleanUpExeDir(exeDir, exePath);
ProcessBuilder pb = new ProcessBuilder(exePath);
synchronized (EngineUtil.nativeLock) {
engineProc = pb.start();
@@ -166,6 +168,22 @@ public class ExternalEngine extends UCIEngineBase {
}
}
+ /** Remove all files except exePath from exeDir. */
+ private void cleanUpExeDir(File exeDir, String exePath) {
+ try {
+ exePath = new File(exePath).getCanonicalPath();
+ File[] files = exeDir.listFiles();
+ if (files == null)
+ return;
+ for (File f : files) {
+ if (!f.getCanonicalPath().equals(exePath))
+ f.delete();
+ }
+ new File(context.getFilesDir(), "engine.exe").delete();
+ } catch (IOException e) {
+ }
+ }
+
private int hashMB = -1;
private String gaviotaTbPath = "";
private String syzygyPath = "";
@@ -262,10 +280,11 @@ public class ExternalEngine extends UCIEngineBase {
stdErrThread.interrupt();
}
- protected void copyFile(File from, File to) throws IOException {
+ protected String copyFile(File from, File exeDir) throws IOException {
+ File to = new File(exeDir, "engine.exe");
new File(internalSFPath()).delete();
if (to.exists() && (from.length() == to.length()) && (from.lastModified() == to.lastModified()))
- return;
+ return to.getAbsolutePath();
if (to.exists())
to.delete();
to.createNewFile();
@@ -282,6 +301,7 @@ public class ExternalEngine extends UCIEngineBase {
if (outFC != null) { try { outFC.close(); } catch (IOException ex) {} }
to.setLastModified(from.lastModified());
}
+ return to.getAbsolutePath();
}
private final void chmod(String exePath) throws IOException {
diff --git a/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java b/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java
index 99cea10..869ecae 100644
--- a/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java
+++ b/DroidFish/src/org/petero/droidfish/engine/InternalStockFish.java
@@ -29,7 +29,6 @@ import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
-import java.util.zip.GZIPInputStream;
import android.content.Context;
import android.os.Environment;
@@ -95,8 +94,6 @@ public class InternalStockFish extends ExternalEngine {
InputStream is = null;
try {
is = context.getAssets().open(sfExe);
- if (sfExe.endsWith(".mygz"))
- is = new GZIPInputStream(is);
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] buf = new byte[8192];
while (true) {
@@ -121,7 +118,8 @@ public class InternalStockFish extends ExternalEngine {
}
@Override
- protected void copyFile(File from, File to) throws IOException {
+ protected String copyFile(File from, File exeDir) throws IOException {
+ File to = new File(exeDir, "engine.exe");
final String sfExe = EngineUtil.internalStockFishName();
// The checksum test is to avoid writing to /data unless necessary,
@@ -129,15 +127,13 @@ public class InternalStockFish extends ExternalEngine {
long oldCSum = readCheckSum(new File(internalSFPath()));
long newCSum = computeAssetsCheckSum(sfExe);
if (oldCSum == newCSum)
- return;
+ return to.getAbsolutePath();
if (to.exists())
to.delete();
to.createNewFile();
InputStream is = context.getAssets().open(sfExe);
- if (sfExe.endsWith(".mygz"))
- is = new GZIPInputStream(is);
OutputStream os = new FileOutputStream(to);
try {
@@ -154,5 +150,6 @@ public class InternalStockFish extends ExternalEngine {
}
writeCheckSum(new File(internalSFPath()), newCSum);
+ return to.getAbsolutePath();
}
}
diff --git a/DroidFish/src/org/petero/droidfish/engine/OpenExchangeEngine.java b/DroidFish/src/org/petero/droidfish/engine/OpenExchangeEngine.java
new file mode 100644
index 0000000..d487850
--- /dev/null
+++ b/DroidFish/src/org/petero/droidfish/engine/OpenExchangeEngine.java
@@ -0,0 +1,32 @@
+package org.petero.droidfish.engine;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import com.kalab.chess.enginesupport.ChessEngine;
+import com.kalab.chess.enginesupport.ChessEngineResolver;
+
+import android.content.Context;
+
+/** Engine imported from a different android app, resolved using the open exchange format. */
+public class OpenExchangeEngine extends ExternalEngine {
+
+ public OpenExchangeEngine(Context context, String engine, Report report) {
+ super(context, engine, report);
+ }
+
+ @Override
+ protected String copyFile(File from, File exeDir) throws IOException {
+ new File(internalSFPath()).delete();
+ ChessEngineResolver resolver = new ChessEngineResolver(context);
+ List engines = resolver.resolveEngines();
+ for (ChessEngine engine : engines) {
+ if (EngineUtil.openExchangeFileName(engine).equals(from.getName())) {
+ File engineFile = engine.copyToFiles(context.getContentResolver(), exeDir);
+ return engineFile.getAbsolutePath();
+ }
+ }
+ throw new IOException("Engine not found");
+ }
+}
diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java
index 5a562a1..07fa55f 100644
--- a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java
+++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java
@@ -46,6 +46,8 @@ public abstract class UCIEngineBase implements UCIEngine {
return new CuckooChessEngine(report);
else if ("stockfish".equals(engine))
return new InternalStockFish(context, report);
+ else if (EngineUtil.isOpenExchangeEngine(engine))
+ return new OpenExchangeEngine(context, engine, report);
else if (EngineUtil.isNetEngine(engine))
return new NetworkEngine(context, engine, engineOptions, report);
else