mirror of
https://github.com/peterosterlund2/droidfish.git
synced 2025-12-12 17:12:40 +01:00
DroidFish: Fixed crash in android 7 when handling large PGN games.
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
<paths>
|
||||
<files-path path="images/" name="myimages"/>
|
||||
<files-path path="shared/" name="myshared"/>
|
||||
</paths>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
175
DroidFish/src/org/petero/droidfish/ObjectCache.java
Normal file
175
DroidFish/src/org/petero/droidfish/ObjectCache.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
133
DroidFishTest/src/org/petero/droidfish/ObjectCacheTest.java
Normal file
133
DroidFishTest/src/org/petero/droidfish/ObjectCacheTest.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user