diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java index 3e4236db5..96eeb533f 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java @@ -1106,24 +1106,27 @@ public class DBUtils { public static byte[] loadGame( Context context, GameLock lock ) { + byte[] result = null; long rowid = lock.getRowid(); Assert.assertTrue( ROWID_NOTFOUND != rowid ); - byte[] result = getCached( rowid ); - if ( null == result ) { - String[] columns = { DBHelper.SNAPSHOT }; - String selection = String.format( ROW_ID_FMT, rowid ); - initDB( context ); - synchronized( s_dbHelper ) { - Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); - if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { - result = cursor.getBlob( cursor - .getColumnIndex(DBHelper.SNAPSHOT)); - } else { - Log.e( TAG, "loadGame: none for rowid=%d", rowid ); + if ( Quarantine.safeToOpen( rowid ) ) { + result = getCached( rowid ); + if ( null == result ) { + String[] columns = { DBHelper.SNAPSHOT }; + String selection = String.format( ROW_ID_FMT, rowid ); + initDB( context ); + synchronized( s_dbHelper ) { + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); + if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { + result = cursor.getBlob( cursor + .getColumnIndex(DBHelper.SNAPSHOT)); + } else { + Log.e( TAG, "loadGame: none for rowid=%d", rowid ); + } + cursor.close(); } - cursor.close(); + setCached( rowid, result ); } - setCached( rowid, result ); } return result; } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java index 8a1ebac0f..0792253a3 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java @@ -53,6 +53,7 @@ public class DlgDelegate { SEND_EMAIL, WRITE_LOG_DB, CLEAR_LOG_DB, + CLEAR_QUARANTINE, // BoardDelegate UNDO_LAST_ACTION, diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java index d0746fb50..5e5bda7af 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java @@ -1226,6 +1226,25 @@ public class GamesListDelegate extends ListDelegateBase } ); } + private void openWithChecks( long rowid, GameSummary summary ) + { + if ( ! m_launchedGames.contains( rowid ) ) { + if ( Quarantine.safeToOpen( rowid ) ) { + makeNotAgainBuilder( R.string.not_again_newselect, + R.string.key_notagain_newselect, + Action.OPEN_GAME ) + .setParams( rowid, summary ) + .show(); + } else { + makeConfirmThenBuilder( R.string.unsafe_open_warning, + Action.CLEAR_QUARANTINE ) + .setPosButton( R.string.unsafe_open_disregard ) + .setParams( rowid, summary ) + .show(); + } + } + } + ////////////////////////////////////////////////////////////////////// // SelectableItem interface ////////////////////////////////////////////////////////////////////// @@ -1237,13 +1256,7 @@ public class GamesListDelegate extends ListDelegateBase // an empty room name. if ( clicked instanceof GameListItem ) { long rowid = ((GameListItem)clicked).getRowID(); - if ( ! m_launchedGames.contains( rowid ) ) { - makeNotAgainBuilder( R.string.not_again_newselect, - R.string.key_notagain_newselect, - Action.OPEN_GAME ) - .setParams( rowid, summary ) - .show(); - } + openWithChecks( rowid, summary ); } } @@ -1344,6 +1357,13 @@ public class GamesListDelegate extends ListDelegateBase case OPEN_GAME: doOpenGame( params ); break; + case CLEAR_QUARANTINE: + long rowid = (long)params[0]; + Quarantine.clear( rowid ); + GameSummary summary = (GameSummary)params[0]; + openWithChecks( rowid, summary ); + break; + case CLEAR_SELS: clearSelections(); break; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Quarantine.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Quarantine.java new file mode 100644 index 000000000..94c1cd620 --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Quarantine.java @@ -0,0 +1,143 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2009-2020 by Eric House (xwords@eehouse.org). All rights + * reserved. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.eehouse.android.xw4; + +import android.content.Context; + +import java.util.HashMap; +import java.util.Map; +import java.io.Serializable; + +public class Quarantine { + private static final String TAG = Quarantine.class.getSimpleName(); + private static final String DATA_KEY = TAG + "/key"; + + public static boolean safeToOpen( long rowid ) + { + int count; + synchronized ( sDataRef ) { + count = get().getFor( rowid ); + } + boolean result = count == 0; // Not too strict? + Log.d( TAG, "safeToOpen(%d) => %b (count=%d)", rowid, result, count ); + return result; + } + + public static void clear( long rowid ) + { + synchronized ( sDataRef ) { + get().clear( rowid ); + store(); + } + } + + public static void recordOpened( long rowid ) + { + synchronized ( sDataRef ) { + get().increment( rowid ); + store(); + Log.d( TAG, "recordOpened(%d): %s", rowid, sDataRef[0].toString() ); + } + } + + public static void recordClosed( long rowid ) + { + synchronized ( sDataRef ) { + get().decrement( rowid ); + store(); + Log.d( TAG, "recordClosed(%d): %s", rowid, sDataRef[0].toString() ); + } + } + + private static class Data implements Serializable { + private HashMap mCounts = new HashMap<>(); + + synchronized void increment( long rowid ) { + if ( ! mCounts.containsKey(rowid) ) { + mCounts.put(rowid, 0); + } + mCounts.put( rowid, mCounts.get(rowid) + 1 ); + } + + synchronized void decrement( long rowid ) + { + Assert.assertTrue( mCounts.containsKey(rowid) ); + mCounts.put( rowid, mCounts.get(rowid) - 1 ); + Assert.assertTrueNR( mCounts.get(rowid) >= 0 ); + } + + synchronized int getFor( long rowid ) + { + int result = mCounts.containsKey(rowid) ? mCounts.get( rowid ) : 0; + return result; + } + + synchronized void clear( long rowid ) + { + mCounts.put( rowid, 0 ); + } + + @Override + synchronized public String toString() + { + StringBuilder sb = new StringBuilder().append("["); + synchronized ( mCounts ) { + for ( long rowid : mCounts.keySet() ) { + int count = mCounts.get(rowid); + sb.append( String.format("{%d: %d}", rowid, count ) ); + } + } + return sb.append("]").toString(); + } + } + + private static Data[] sDataRef = {null}; + + private static void store() + { + synchronized( sDataRef ) { + DBUtils.setSerializableFor( getContext(), DATA_KEY, sDataRef[0] ); + } + } + + private static Data get() + { + Data data; + synchronized ( sDataRef ) { + data = sDataRef[0]; + if ( null == data ) { + data = (Data)DBUtils.getSerializableFor( getContext(), DATA_KEY ); + if ( null == data ) { + data = new Data(); + } else { + Log.d( TAG, "loading existing: %s", data ); + } + sDataRef[0] = data; + } + } + return data; + } + + private static Context getContext() + { + return XWApp.getContext(); + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java index 1db3931e9..6e46a5338 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java @@ -28,6 +28,7 @@ import org.eehouse.android.xw4.Assert; import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.NetLaunchInfo; +import org.eehouse.android.xw4.Quarantine; import org.eehouse.android.xw4.Utils; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; @@ -46,6 +47,7 @@ public class XwJNI { m_ptrGame = ptr; m_rowid = rowid; mStack = android.util.Log.getStackTraceString(new Exception()); + Quarantine.recordOpened( rowid ); } public synchronized long ptr() @@ -73,6 +75,7 @@ public class XwJNI { // getClass().getName(), this, m_rowid, m_refCount ); if ( 0 == m_refCount ) { if ( 0 != m_ptrGame ) { + Quarantine.recordClosed( m_rowid ); if ( haveEnv( getJNI().m_ptrGlobals ) ) { game_dispose( this ); // will crash if haveEnv fails } else { diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 61c5746a5..3e232ecde 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -2520,6 +2520,11 @@ Message: %1$s. Auto-paused. + This game has caused CrossWords + to crash recently and is likely corrupt. Do you want to open it + anyway? + Open anyway + Debug logs Enable log storage