From a620ae4afc1511d38397302d444252568591a26a Mon Sep 17 00:00:00 2001 From: Eric House Date: Tue, 7 Jun 2022 21:59:44 -0700 Subject: [PATCH] use ContentResolver to export and import backups This is the kosher way now, and solves problems with accessing Downloads or other public directories on newer Android versions. --- .../java/org/eehouse/android/xw4/DBUtils.java | 86 +++++++++---------- .../org/eehouse/android/xw4/DictUtils.java | 7 +- .../android/xw4/GamesListDelegate.java | 63 ++++++++++---- .../org/eehouse/android/xw4/RequestCode.java | 2 + .../app/src/main/res/values/strings.xml | 8 +- 5 files changed, 96 insertions(+), 70 deletions(-) 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 06435c061..c04b37d71 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 @@ -32,6 +32,7 @@ import android.database.sqlite.SQLiteStatement; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.Environment; import android.text.TextUtils; @@ -52,8 +53,9 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; -import java.nio.channels.FileChannel; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; @@ -1851,38 +1853,61 @@ public class DBUtils { } } - public static boolean loadDB( Context context ) + public static boolean loadDB( Context context, Uri uri ) { - boolean success = copyGameDB( context, false ); + boolean success = false; + try ( InputStream is = context + .getContentResolver().openInputStream(uri) ) { + String name = DBHelper.getDBName(); + File gamesDB = context.getDatabasePath( name ); + FileOutputStream fos = new FileOutputStream( gamesDB ); + success = copyStream( fos, is ); + invalGroupsCache(); + } catch ( Exception ex ) { + Log.ex( TAG, ex ); + } + if ( success ) { PrefsDelegate.loadPrefs( context ); } return success; } - public static boolean saveDB( Context context ) + public static boolean saveDB( Context context, Uri uri ) { PrefsDelegate.savePrefs( context ); - return copyGameDB( context, true ); + boolean success = false; + try ( OutputStream os = context.getContentResolver().openOutputStream( uri ) ) { + String name = DBHelper.getDBName(); + File gamesDB = context.getDatabasePath( name ); + FileInputStream fis = new FileInputStream( gamesDB ); + success = copyStream( os, fis ); + } catch ( Exception ex ) { + Log.ex( TAG, ex ); + } + return success; } - public static boolean copyFileStream( FileOutputStream fos, - FileInputStream fis ) + public static boolean copyStream( OutputStream fos, InputStream fis ) { boolean success = false; - FileChannel channelSrc = null; - FileChannel channelDest = null; + byte[] buf = new byte[1024*8]; try { - channelSrc = fis.getChannel(); - channelDest = fos.getChannel(); - channelSrc.transferTo( 0, channelSrc.size(), channelDest ); + for ( ; ; ) { + int nRead = fis.read( buf ); + if ( 0 >= nRead ) { + break; + } + fos.write( buf, 0, nRead ); + } success = true; + Log.d( TAG, "copyFileStream(): copied %s to %s", fis, fos ); } catch( java.io.IOException ioe ) { Log.ex( TAG, ioe ); } finally { try { - channelSrc.close(); - channelDest.close(); + fos.close(); + fis.close(); } catch( java.io.IOException ioe ) { Log.ex( TAG, ioe ); } @@ -2448,37 +2473,6 @@ public class DBUtils { } } - private static boolean copyGameDB( Context context, boolean toSDCard ) - { - boolean success = false; - String name = DBHelper.getDBName(); - File gamesDB = context.getDatabasePath( name ); - - // Use the variant name EXCEPT where we're copying from sdCard and - // only the older name exists. - File sdcardDB = new File( Environment.getExternalStorageDirectory(), - getVariantDBName() ); - if ( !toSDCard && !sdcardDB.exists() ) { - sdcardDB = new File( Environment.getExternalStorageDirectory(), - name ); - } - - try { - File srcDB = toSDCard? gamesDB : sdcardDB; - if ( srcDB.exists() ) { - FileInputStream src = new FileInputStream( srcDB ); - FileOutputStream dest = - new FileOutputStream( toSDCard? sdcardDB : gamesDB ); - copyFileStream( dest, src ); - invalGroupsCache(); - success = true; - } - } catch( java.io.FileNotFoundException fnfe ) { - Log.ex( TAG, fnfe ); - } - return success; - } - // Copy my .apk to the Downloads directory, from which a user could more // easily share it with somebody else. Should be blocked for apks // installed from the Play store since viral distribution isn't allowed, @@ -2499,7 +2493,7 @@ public class DBUtils { FileInputStream src = new FileInputStream( srcPath ); FileOutputStream dest = new FileOutputStream( destPath ); - copyFileStream( dest, src ); + copyStream( dest, src ); } catch ( Exception ex ) { Log.e( TAG, "copyApkToDownloads(): got ex: %s", ex ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictUtils.java index 1bcc87e2a..6c09315f9 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictUtils.java @@ -303,7 +303,7 @@ public class DictUtils { ? context.openFileOutput( name, Context.MODE_PRIVATE ) : new FileOutputStream( getDictFile( context, name, to ) ); - success = DBUtils.copyFileStream( fos, fis ); + success = DBUtils.copyStream( fos, fis ); } catch ( java.io.FileNotFoundException fnfe ) { Log.ex( TAG, fnfe ); } @@ -663,7 +663,7 @@ public class DictUtils { { File result = null; outer: - for ( int attempt = 0; attempt < 4; ++attempt ) { + for ( int attempt = 0; ; ++attempt ) { switch ( attempt ) { case 0: String myPath = XWPrefs.getMyDownloadDir( context ); @@ -679,7 +679,6 @@ public class DictUtils { result = s_dirGetter.getDownloadDir(); break; case 2: - case 3: if ( !haveWriteableSD() ) { continue; } @@ -689,6 +688,8 @@ public class DictUtils { result = new File( result, "download/" ); } break; + default: + break outer; } // Exit test for loop 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 f21f605e9..5b42f648a 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 @@ -1522,21 +1522,6 @@ public class GamesListDelegate extends ListDelegateBase rematchWithNameAndPerm( true, params ); break; - case STORAGE_CONFIRMED: - int id = (Integer)params[0]; - if ( R.id.games_menu_loaddb == id ) { - DBUtils.loadDB( m_activity ); - storeGroupPositions( null ); - mkListAdapter(); - } else if ( R.id.games_menu_storedb == id ) { - int msgID = DBUtils.saveDB( m_activity ) - ? R.string.db_store_done : R.string.db_store_failed; - showToast( msgID ); - } else { - Assert.failDbg(); - } - break; - case APPLY_CONFIG: Uri data = Uri.parse( (String)params[0] ); CommonPrefs.loadColorPrefs( m_activity, data ); @@ -1561,6 +1546,43 @@ public class GamesListDelegate extends ListDelegateBase return handled; } + private void startLoadOrStore( boolean isStore ) + { + String intentAction = null; + RequestCode rq = null; + if ( isStore ) { + intentAction = Intent.ACTION_CREATE_DOCUMENT; + rq = RequestCode.STORE_DATA_FILE; + } else { + intentAction = Intent.ACTION_OPEN_DOCUMENT; + rq = RequestCode.LOAD_DATA_FILE; + } + Intent intent = new Intent( intentAction ); + intent.addCategory( Intent.CATEGORY_OPENABLE ); + intent.setType( "application/octet-stream" ); + if ( isStore ) { + intent.putExtra( Intent.EXTRA_TITLE, DBHelper.getDBName() ); + } + startActivityForResult( intent, rq ); + } + + private void handleLoadOrStoreResult( Uri uri, boolean isStore ) + { + if ( isStore ) { + boolean saved = DBUtils.saveDB( m_activity, uri ); + int msgID = saved ? R.string.db_store_done + : R.string.db_store_failed; + showToast( msgID ); + } else { + if ( DBUtils.loadDB( m_activity, uri ) ) { + storeGroupPositions( null ); + mkListAdapter(); + // We really want to exit the app!!! PENDING + } + } + } + + @Override public boolean onNegButton( Action action, Object[] params ) { @@ -1602,6 +1624,14 @@ public class GamesListDelegate extends ListDelegateBase launchGame( rowID ); } break; + case STORE_DATA_FILE: + case LOAD_DATA_FILE: + if ( Activity.RESULT_OK == resultCode && data != null ) { + boolean isStore = RequestCode.STORE_DATA_FILE == requestCode; + Uri uri = data.getData(); + handleLoadOrStoreResult( uri, isStore ); + } + break; } } @@ -1813,8 +1843,7 @@ public class GamesListDelegate extends ListDelegateBase case R.id.games_menu_loaddb: case R.id.games_menu_storedb: - Perms23.tryGetPerms( this, Perm.STORAGE, null, - Action.STORAGE_CONFIRMED, itemID ); + startLoadOrStore( R.id.games_menu_storedb == itemID ); break; case R.id.games_menu_writegit: diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java index efe655237..d60d9de69 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java @@ -42,6 +42,8 @@ public enum RequestCode { // Games list REQUEST_LANG_GL, CONFIG_GAME, + STORE_DATA_FILE, + LOAD_DATA_FILE, // SMSInviteDelegate GET_CONTACT, diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index d23282d82..69daf042f 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -2278,8 +2278,8 @@ MQTT port MQTT QOS %1$s/%2$s - Write games to SD card - Load games from SD card + Export app data + Import app data Copy git info to clipboard Show Pending messages Show number not yet acknowledged @@ -2307,8 +2307,8 @@ Get intermediate builds Checking Checking for wordlists in %1$s… - SD card write complete - SD card write failed + Export complete + Export failed Are you sure you want to drop this game’s ability to communicate via the internet? Bluetooth only works for nearby