DroidFish: Support for using "open exchange" engines installed on the android device.

This commit is contained in:
Peter Osterlund 2014-10-04 08:23:57 +00:00
parent a1047dccea
commit 8609a05d10
9 changed files with 397 additions and 40 deletions

View File

@ -220,6 +220,9 @@
<li>
Color picker, Copyright © 2010 Daniel Nilsson and Copyright © 2011 Sergey Margaritov.
</li>
<li>
Open exchange chess engine interface code by Gerhard Kalab, <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 license</a>.
</li>
</ul>
<h3>Translations</h3>

View File

@ -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;
}
}

View File

@ -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<ChessEngine> resolveEngines() {
List<ChessEngine> result = new ArrayList<ChessEngine>();
final Intent intent = new Intent(ENGINE_PROVIDER_MARKER);
List<ResolveInfo> 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<ChessEngine> resolveEnginesForPackage(
List<ChessEngine> 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<ChessEngine> 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;
}
}

View File

@ -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<ChessEngine> 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<String> items = new ArrayList<String>();
final ArrayList<String> ids = new ArrayList<String>();
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<ChessEngine> engines = resolver.resolveEngines();
ArrayList<Pair<String,String>> oexEngines = new ArrayList<Pair<String,String>>();
for (ChessEngine engine : engines) {
if ((engine.getName() != null) && (engine.getFileName() != null) &&
(engine.getPackageName() != null)) {
oexEngines.add(new Pair<String,String>(EngineUtil.openExchangeFileName(engine),
engine.getName()));
}
}
Collections.sort(oexEngines, new Comparator<Pair<String,String>>() {
@Override
public int compare(Pair<String, String> lhs, Pair<String, String> rhs) {
return lhs.second.compareTo(rhs.second);
}
});
for (Pair<String,String> 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();

View File

@ -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);

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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<ChessEngine> 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");
}
}

View File

@ -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