diff --git a/DroidFish/res/xml/filepaths.xml b/DroidFish/res/xml/filepaths.xml index b73a289..8097df4 100644 --- a/DroidFish/res/xml/filepaths.xml +++ b/DroidFish/res/xml/filepaths.xml @@ -1,3 +1,3 @@ - + diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index fc0101d..7d93f3d 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -27,6 +27,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -225,6 +226,7 @@ public class DroidFish extends Activity private ListView rightDrawer; private SharedPreferences settings; + private ObjectCache cache; private float scrollSensitivity; private boolean invertScrollDirection; @@ -475,6 +477,7 @@ public class DroidFish extends Activity PreferenceManager.setDefaultValues(this, R.xml.preferences, false); settings = PreferenceManager.getDefaultSharedPreferences(this); + cache = new ObjectCache(); setWakeLock(false); @@ -506,7 +509,9 @@ public class DroidFish extends Activity byte[] data = null; int version = 1; if (savedInstanceState != null) { - data = savedInstanceState.getByteArray("gameState"); + byte[] token = savedInstanceState.getByteArray("gameStateT"); + if (token != null) + data = cache.retrieveBytes(token); version = savedInstanceState.getInt("gameStateVersion", version); } else { String dataStr = settings.getString("gameState", null); @@ -1125,7 +1130,8 @@ public class DroidFish extends Activity super.onSaveInstanceState(outState); if (ctrl != null) { byte[] data = ctrl.toByteArray(); - outState.putByteArray("gameState", data); + byte[] token = data == null ? null : cache.storeBytes(data); + outState.putByteArray("gameStateT", token); outState.putInt("gameStateVersion", 3); } } @@ -1693,7 +1699,8 @@ public class DroidFish extends Activity case RESULT_LOAD_PGN: if (resultCode == RESULT_OK) { try { - String pgn = data.getAction(); + String pgnToken = data.getAction(); + String pgn = cache.retrieveString(pgnToken); int modeNr = ctrl.getGameMode().getModeNr(); if ((modeNr != GameMode.ANALYSIS) && (modeNr != GameMode.EDIT_GAME)) newGameMode(GameMode.EDIT_GAME); @@ -2337,7 +2344,29 @@ public class DroidFish extends Activity Intent i = new Intent(Intent.ACTION_SEND); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); i.setType(game ? "application/x-chess-pgn" : "text/plain"); - i.putExtra(Intent.EXTRA_TEXT, ctrl.getPGN()); + String pgn = ctrl.getPGN(); + if (pgn.length() < 32768) { + i.putExtra(Intent.EXTRA_TEXT, pgn); + } else { + File dir = new File(getFilesDir(), "shared"); + dir.mkdirs(); + File file = new File(dir, game ? "game.pgn" : "game.txt"); + try { + FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter ow = new OutputStreamWriter(fos, "UTF-8"); + try { + ow.write(pgn); + } finally { + ow.close(); + } + } catch (IOException e) { + Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + return; + } + String authority = "org.petero.droidfish.fileprovider"; + Uri uri = FileProvider.getUriForFile(this, authority, file); + i.putExtra(Intent.EXTRA_STREAM, uri); + } try { startActivity(Intent.createChooser(i, getString(game ? R.string.share_game : R.string.share_text))); @@ -2351,7 +2380,7 @@ public class DroidFish extends Activity Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); v.draw(c); - File imgDir = new File(getFilesDir(), "images"); + File imgDir = new File(getFilesDir(), "shared"); imgDir.mkdirs(); File file = new File(imgDir, "screenshot.png"); try { @@ -3712,6 +3741,7 @@ public class DroidFish extends Activity /** Save current game to a PGN file. */ private final void savePGNToFile(String pathName, boolean silent) { String pgn = ctrl.getPGN(); + String pgnToken = cache.storeString(pgn); Editor editor = settings.edit(); editor.putString("currentPGNFile", pathName); editor.putInt("currFT", FT_PGN); @@ -3719,7 +3749,7 @@ public class DroidFish extends Activity Intent i = new Intent(DroidFish.this, EditPGNSave.class); i.setAction("org.petero.droidfish.saveFile"); i.putExtra("org.petero.droidfish.pathname", pathName); - i.putExtra("org.petero.droidfish.pgn", pgn); + i.putExtra("org.petero.droidfish.pgn", pgnToken); i.putExtra("org.petero.droidfish.silent", silent); startActivity(i); } diff --git a/DroidFish/src/org/petero/droidfish/ObjectCache.java b/DroidFish/src/org/petero/droidfish/ObjectCache.java new file mode 100644 index 0000000..1ea4774 --- /dev/null +++ b/DroidFish/src/org/petero/droidfish/ObjectCache.java @@ -0,0 +1,175 @@ +/* + DroidFish - An Android chess program. + Copyright (C) 2017 Peter Ă–sterlund, peterosterlund2@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package org.petero.droidfish; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; + +import android.content.Context; + +/** + * Stores large objects temporarily in the file system to avoid + * too large transactions when communicating between activities. + * The cache has a limited size, so trying to retrieve a stored + * object can fail in which case null is returned. */ +public class ObjectCache { + public final static int MAX_MEM_SIZE = 16384; // Max size of object to store in memory + public final static int MAX_CACHED_OBJS = 10; // Max no of objects to cache in file system + private final Context context; + + public ObjectCache() { + this(DroidFishApp.getContext()); + } + + public ObjectCache(Context context) { + this.context = context; + } + + /** Store a string in the cache and return a token that can be + * used to retrieve the original string. */ + public String storeString(String s) { + if (s.length() < MAX_MEM_SIZE) { + return "0" + s; + } else { + long token = storeInCache(s.getBytes()); + return "1" + Long.toString(token); + } + } + + /** Retrieve a string from the cache using a token previously + * returned by storeString(). + * @return The string, or null if not found in the cache. */ + public String retrieveString(String token) { + if (token.startsWith("0")) { + return token.substring(1); + } else { + String tokStr = token.substring(1); + long longTok = Long.valueOf(tokStr); + byte[] buf = retrieveFromCache(longTok); + return buf == null ? null : new String(buf); + } + } + + /** Store a byte array in the cache and return a token that can be + * used to retrieve the original byte array. */ + public byte[] storeBytes(byte[] b) { + if (b.length < MAX_MEM_SIZE) { + byte[] ret = new byte[b.length + 1]; + ret[0] = 0; + System.arraycopy(b, 0, ret, 1, b.length); + return ret; + } else { + long token = storeInCache(b); + byte[] tokBuf = Long.toString(token).getBytes(); + byte[] ret = new byte[1 + tokBuf.length]; + ret[0] = 1; + System.arraycopy(tokBuf, 0, ret, 1, tokBuf.length); + return ret; + } + } + + /** Retrieve a byte array from the cache using a token previously + * returned by storeBytes(). + * @return The byte array, or null if not found in the cache. */ + public byte[] retrieveBytes(byte[] token) { + if (token[0] == 0) { + byte[] ret = new byte[token.length - 1]; + System.arraycopy(token, 1, ret, 0, token.length - 1); + return ret; + } else { + String tokStr = new String(token, 1, token.length - 1); + long longTok = Long.valueOf(tokStr); + return retrieveFromCache(longTok); + } + } + + private final static String cacheDir = "objcache"; + + private long storeInCache(byte[] b) { + File cd = context.getCacheDir(); + File dir = new File(cd, cacheDir); + if (dir.exists() || dir.mkdir()) { + try { + File[] files = dir.listFiles(); + if (files != null) { + long[] tokens = new long[files.length]; + long token = -1; + for (int i = 0; i < files.length; i++) { + try { + tokens[i] = Long.valueOf(files[i].getName()); + token = Math.max(token, tokens[i]); + } catch (NumberFormatException nfe) { + } + } + Arrays.sort(tokens); + for (int i = 0; i < files.length - (MAX_CACHED_OBJS - 1); i++) { + File f = new File(dir, String.valueOf(tokens[i])); + f.delete(); + } + int maxTries = 10; + for (int i = 0; i < maxTries; i++) { + token++; + File f = new File(dir, String.valueOf(token)); + if (f.createNewFile()) { + FileOutputStream fos = new FileOutputStream(f); + try { + fos.write(b); + return token; + } finally { + fos.close(); + } + } + } + } + } catch (IOException e) { + } + } + return -1; + } + + private byte[] retrieveFromCache(long token) { + File cd = context.getCacheDir(); + File dir = new File(cd, cacheDir); + if (dir.exists()) { + File f = new File(dir, String.valueOf(token)); + try { + RandomAccessFile raf = new RandomAccessFile(f, "r"); + try { + int len = (int)raf.length(); + byte[] buf = new byte[len]; + int offs = 0; + while (offs < len) { + int l = raf.read(buf, offs, len - offs); + if (l <= 0) + return null; + offs += l; + } + return buf; + } finally { + raf.close(); + } + } catch (IOException ex) { + } + } + return null; + } +} diff --git a/DroidFish/src/org/petero/droidfish/activities/EditPGN.java b/DroidFish/src/org/petero/droidfish/activities/EditPGN.java index 03a8e6c..dd276e2 100644 --- a/DroidFish/src/org/petero/droidfish/activities/EditPGN.java +++ b/DroidFish/src/org/petero/droidfish/activities/EditPGN.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Locale; import org.petero.droidfish.ColorTheme; +import org.petero.droidfish.ObjectCache; import org.petero.droidfish.R; import org.petero.droidfish.Util; import org.petero.droidfish.activities.PGNFile.GameInfo; @@ -160,7 +161,8 @@ public class EditPGN extends ListActivity { } } else if (action.equals("org.petero.droidfish.saveFile")) { loadGame = false; - pgnToSave = i.getStringExtra("org.petero.droidfish.pgn"); + String token = i.getStringExtra("org.petero.droidfish.pgn"); + pgnToSave = (new ObjectCache()).retrieveString(token); boolean silent = i.getBooleanExtra("org.petero.droidfish.silent", false); if (silent) { // Silently append to file PGNFile pgnFile2 = new PGNFile(fileName); @@ -466,7 +468,8 @@ public class EditPGN extends ListActivity { private final void sendBackResult(GameInfo gi) { String pgn = pgnFile.readOneGame(gi); if (pgn != null) { - setResult(RESULT_OK, (new Intent()).setAction(pgn)); + String pgnToken = (new ObjectCache()).storeString(pgn); + setResult(RESULT_OK, (new Intent()).setAction(pgnToken)); finish(); } else { setResult(RESULT_CANCELED); diff --git a/DroidFish/src/org/petero/droidfish/activities/LoadScid.java b/DroidFish/src/org/petero/droidfish/activities/LoadScid.java index 82d68dd..43c077b 100644 --- a/DroidFish/src/org/petero/droidfish/activities/LoadScid.java +++ b/DroidFish/src/org/petero/droidfish/activities/LoadScid.java @@ -24,6 +24,7 @@ import java.util.Vector; import java.util.concurrent.CountDownLatch; import org.petero.droidfish.ColorTheme; +import org.petero.droidfish.ObjectCache; import org.petero.droidfish.R; import org.petero.droidfish.Util; @@ -371,7 +372,8 @@ public class LoadScid extends ListActivity { if (cursor != null && cursor.moveToFirst()) { String pgn = cursor.getString(cursor.getColumnIndex("pgn")); if (pgn != null && pgn.length() > 0) { - setResult(RESULT_OK, (new Intent()).setAction(pgn)); + String pgnToken = (new ObjectCache()).storeString(pgn); + setResult(RESULT_OK, (new Intent()).setAction(pgnToken)); finish(); return; } diff --git a/DroidFishTest/src/org/petero/droidfish/ObjectCacheTest.java b/DroidFishTest/src/org/petero/droidfish/ObjectCacheTest.java new file mode 100644 index 0000000..97252a5 --- /dev/null +++ b/DroidFishTest/src/org/petero/droidfish/ObjectCacheTest.java @@ -0,0 +1,133 @@ +/* + DroidFish - An Android chess program. + Copyright (C) 2017 Peter Ă–sterlund, peterosterlund2@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package org.petero.droidfish; + + +import java.util.Arrays; + +import junit.framework.TestCase; + + +public class ObjectCacheTest extends TestCase { + public ObjectCacheTest() { + } + + public void testCache() { + ObjectCache cache = new ObjectCache(DroidFishApp.getContext()); + final int M = ObjectCache.MAX_MEM_SIZE; + final int N = ObjectCache.MAX_CACHED_OBJS; + { // Test small string + String s0 = "testing"; + String token = cache.storeString(s0); + String s = cache.retrieveString(token); + assertEquals(s0, s); + } + { // Test small byte array + byte[] b0 = {1,2,3,4,5}; + byte[] token = cache.storeBytes(b0); + byte[] b = cache.retrieveBytes(token); + assertTrue(Arrays.equals(b0, b)); + } + { // Test large string + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < M + 1; i++) + sb.append('a'); + String s0 = sb.toString(); + String token = cache.storeString(s0); + String s = cache.retrieveString(token); + assertEquals(s0, s); + } + { // Test large byte array + byte[] b0 = new byte[M+1]; + for (int i = 0; i < M + 1; i++) + b0[i] = 'a'; + byte[] token = cache.storeBytes(b0); + byte[] b = cache.retrieveBytes(token); + assertTrue(Arrays.equals(b0, b)); + } + { // Test large string objects + String[] s0 = new String[N]; + String[] tokens = new String[N]; + for (int i = 0; i < N; i++) { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < M + 1 + i * 100; j++) + sb.append((char)((i + j) % 255)); + s0[i] = sb.toString(); + tokens[i] = cache.storeString(s0[i]); + } + { // Small objects must not evict older entries + for (int i = 0; i < 100; i++) + cache.storeString("abc"); + for (int i = 0; i < 100; i++) + cache.storeBytes(new byte[]{(byte)i,(byte)(i*2),(byte)(i+1)}); + } + for (int i = 0; i < N; i++) { + String s = cache.retrieveString(tokens[i]); + assertEquals(s0[i], s); + } + } + { // Test large byte arrays + byte[][] b0 = new byte[N][]; + byte[][] tokens = new byte[N][]; + for (int i = 0; i < N; i++) { + byte[] b = new byte[M + 1 + i * 100]; + for (int j = 0; j < b.length; j++) + b[j] = (byte)((i + j) % 255); + b0[i] = b; + tokens[i] = cache.storeBytes(b0[i]); + } + { // Small objects must not evict older entries + for (int i = 0; i < 100; i++) + cache.storeString("abc"); + for (int i = 0; i < 100; i++) + cache.storeBytes(new byte[]{(byte)i,(byte)(i*2),(byte)(i+1)}); + } + for (int i = 0; i < N; i++) { + byte[] b = cache.retrieveBytes(tokens[i]); + assertTrue(Arrays.equals(b0[i], b)); + } + } + + { // Test that not too many file system objects are used + String[] s0 = new String[N]; + String[] tokens = new String[N]; + for (int i = 0; i < N; i++) { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < M + 1 + i * 100; j++) + sb.append((char)((i + j) % 255)); + s0[i] = sb.toString(); + tokens[i] = cache.storeString(s0[i]); + } + { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < M + 1; j++) + sb.append((char)((j + 3) % 255)); + String s = sb.toString(); + String token = cache.storeString(s); + String s1 = cache.retrieveString(token); + assertEquals(s, s1); + } + assertEquals(null, cache.retrieveString(tokens[0])); + for (int i = 1; i < N; i++) { + String s = cache.retrieveString(tokens[i]); + assertEquals(s0[i], s); + } + } + } +}