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.
This commit is contained in:
Eric House 2022-06-07 21:59:44 -07:00
parent c329b1bab4
commit a620ae4afc
5 changed files with 96 additions and 70 deletions

View file

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

View file

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

View file

@ -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:

View file

@ -42,6 +42,8 @@ public enum RequestCode {
// Games list
REQUEST_LANG_GL,
CONFIG_GAME,
STORE_DATA_FILE,
LOAD_DATA_FILE,
// SMSInviteDelegate
GET_CONTACT,

View file

@ -2278,8 +2278,8 @@
<string name="mqtt_port">MQTT port</string>
<string name="mqtt_qos">MQTT QOS</string>
<string name="name_dict_fmt">%1$s/%2$s</string>
<string name="gamel_menu_storedb">Write games to SD card</string>
<string name="gamel_menu_loaddb">Load games from SD card</string>
<string name="gamel_menu_storedb">Export app data</string>
<string name="gamel_menu_loaddb">Import app data</string>
<string name="gamel_menu_writegit">Copy git info to clipboard</string>
<string name="enable_pending_count_title">Show Pending messages</string>
<string name="enable_pending_count_summary">Show number not yet acknowledged</string>
@ -2307,8 +2307,8 @@
<string name="pref_item_update_summary">Get intermediate builds</string>
<string name="checking_title">Checking</string>
<string name="checking_for_fmt">Checking for wordlists in %1$s…</string>
<string name="db_store_done">SD card write complete</string>
<string name="db_store_failed">SD card write failed</string>
<string name="db_store_done">Export complete</string>
<string name="db_store_failed">Export failed</string>
<string name="confirm_drop_mqtt">Are you sure you want to drop this
games ability to communicate via the internet?</string>
<string name="confirm_drop_relay_bt">Bluetooth only works for nearby