DroidFish: Fixed crash in android 7 when handling large PGN games.

This commit is contained in:
Peter Osterlund
2017-06-10 21:21:03 +02:00
parent 2f15e7a775
commit 2260f19cc8
6 changed files with 353 additions and 10 deletions

View File

@@ -1,3 +1,3 @@
<paths>
<files-path path="images/" name="myimages"/>
<files-path path="shared/" name="myshared"/>
</paths>

View File

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

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

View File

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

View File

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

View 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);
}
}
}
}