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