diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index 69d55de74..afd93bb0c 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -386,6 +386,7 @@ dependencies { // implementation("com.hivemq:hivemq-mqtt-client:1.3.0") implementation 'com.google.zxing:core:3.3.+' + implementation 'com.jakewharton:process-phoenix:2.1.2' } task mkImages(type: Exec) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupConfigView.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupConfigView.java new file mode 100644 index 000000000..3e31b8a1c --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupConfigView.java @@ -0,0 +1,106 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2022 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.view.ViewGroup; +import android.content.Context; +import android.net.Uri; +// import android.text.TextUtils; +import android.util.AttributeSet; +// import android.view.View; +// import android.widget.AdapterView.OnItemSelectedListener; +// import android.widget.AdapterView; +// import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +// import android.widget.RadioButton; +// import android.widget.RadioGroup; +import android.widget.CheckBox; +// import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +// import org.eehouse.android.xw4.jni.XwJNI; +import org.eehouse.android.xw4.loc.LocUtils; + +import org.eehouse.android.xw4.ZipUtils.SaveWhat; + +public class BackupConfigView extends LinearLayout +{ + private static final String TAG = BackupConfigView.class.getSimpleName(); + + private boolean mIsStore; + private Uri mLoadFile; + private Map mCheckBoxes = new HashMap<>(); + private List mShowWhats; + + public BackupConfigView( Context cx, AttributeSet as ) + { + super( cx, as ); + } + + void init( Uri uri ) + { + mLoadFile = uri; + mIsStore = null == uri; + if ( null != uri ) { + mShowWhats = ZipUtils.getHasWhats( getContext(), uri ); + } + } + + @Override + protected void onFinishInflate() + { + Context context = getContext(); + LinearLayout list = (LinearLayout)findViewById( R.id.whats_list ); + for ( SaveWhat what : SaveWhat.values() ) { + if ( null == mShowWhats || mShowWhats.contains(what) ) { + CheckBox box = (CheckBox) + LocUtils.inflate( context, R.layout.invite_checkbox ); + box.setText( what.toString() ); + mCheckBoxes.put( what, box ); + list.addView( box ); + } + } + } + + String getPosButtonTxt() + { + return mIsStore ? "Save" : "Load"; + } + + public List getSaveWhat() + { + List result = new ArrayList<>(); + for ( SaveWhat what : mCheckBoxes.keySet() ) { + CheckBox box = mCheckBoxes.get( what ); + if ( box.isChecked() ) { + result.add( what ); + Log.d( TAG, "getSaveWhat(): added %s", what ); + } else { + Log.d( TAG, "getSaveWhat(): DID NOT add %s", what ); + } + } + Log.d( TAG, "getSaveWhat() => %s", result ); + return result; + } + +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupView.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupView.java new file mode 100644 index 000000000..7f766256f --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BackupView.java @@ -0,0 +1,5 @@ + + + +public class BackupView { +} 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 0837881b1..2c81973da 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 @@ -1853,41 +1853,6 @@ public class DBUtils { } } - public static boolean loadDB( Context context, Uri uri ) - { - 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, Uri uri ) - { - PrefsDelegate.savePrefs( context ); - 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 copyStream( OutputStream fos, InputStream fis ) { boolean success = false; @@ -1904,13 +1869,6 @@ public class DBUtils { Log.d( TAG, "copyFileStream(): copied %s to %s", fis, fos ); } catch( java.io.IOException ioe ) { Log.ex( TAG, ioe ); - } finally { - try { - fos.close(); - fis.close(); - } catch( java.io.IOException ioe ) { - Log.ex( TAG, ioe ); - } } return success; } 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 6c09315f9..54334cbfd 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 @@ -24,14 +24,15 @@ import android.content.Context; import android.content.res.AssetManager; import android.os.Environment; - import org.eehouse.android.xw4.jni.JNIUtilsImpl; import org.eehouse.android.xw4.jni.XwJNI; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; @@ -233,9 +234,9 @@ public class DictUtils { FileInputStream fis = context.openFileInput( name ); fis.close(); loc = DictLoc.INTERNAL; - } catch ( java.io.FileNotFoundException fnf ) { + } catch ( FileNotFoundException fnf ) { // Log.ex( fnf ); - } catch ( java.io.IOException ioe ) { + } catch ( IOException ioe ) { Log.ex( TAG, ioe ); } } @@ -304,8 +305,10 @@ public class DictUtils { : new FileOutputStream( getDictFile( context, name, to ) ); success = DBUtils.copyStream( fos, fis ); - } catch ( java.io.FileNotFoundException fnfe ) { - Log.ex( TAG, fnfe ); + fos.close(); + fis.close(); + } catch ( IOException ex ) { + Log.ex( TAG, ex ); } return success; } // copyDict @@ -368,7 +371,7 @@ public class DictUtils { Assert.assertTrue( -1 == dict.read() ); bytes = bas.toByteArray(); - } catch ( java.io.IOException ee ){ + } catch ( IOException ee ){ } } @@ -403,9 +406,9 @@ public class DictUtils { fis.read( bytes, 0, len ); fis.close(); Log.i( TAG, "Successfully loaded %s", name ); - } catch ( java.io.FileNotFoundException fnf ) { + } catch ( FileNotFoundException fnf ) { // Log.ex( fnf ); - } catch ( java.io.IOException ioe ) { + } catch ( IOException ioe ) { Log.ex( TAG, ioe ); } } @@ -518,9 +521,9 @@ public class DictUtils { if ( success ) { invalDictList(); } - } catch ( java.io.FileNotFoundException fnf ) { + } catch ( FileNotFoundException fnf ) { Log.ex( TAG, fnf ); - } catch ( java.io.IOException ioe ) { + } catch ( IOException ioe ) { Log.ex( TAG, ioe ); tmpFile.delete(); } @@ -607,7 +610,7 @@ public class DictUtils { try { AssetManager am = context.getAssets(); return am.list(""); - } catch( java.io.IOException ioe ) { + } catch( IOException ioe ) { Log.ex( TAG, ioe ); return new String[0]; } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java index 8e321818a..cda9ae922 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java @@ -21,55 +21,56 @@ package org.eehouse.android.xw4; public enum DlgID { - NONE - , CHANGE_GROUP - , CONFIRM_CHANGE - , CONFIRM_CHANGE_PLAY - , CONFIRM_THEN - , DIALOG_NOTAGAIN - , DIALOG_OKONLY - , DIALOG_ENABLESMS - , DICT_OR_DECLINE - , DLG_CONNSTAT - , DLG_DELETED - , DLG_INVITE(true) - , DLG_OKONLY - , ENABLE_NFC - , FORCE_REMOTE - , GET_NAME - , GET_NUMBER - , INVITE_CHOICES_THEN - , MOVE_DICT - , NAME_GAME - , NEW_GROUP - , PLAYER_EDIT - , ENABLE_SMS - , QUERY_ENDGAME - , RENAME_GAME - , RENAME_GROUP - , REVERT_ALL - , REVERT_COLORS - , SET_DEFAULT - , SHOW_SUBST - , WARN_NODICT_GENERIC // the general trying-to-open case - , WARN_NODICT_INVITED // when responding to invitation - , WARN_NODICT_SUBST // when a substitution will be possible/suggested - , DLG_BADWORDS - , NOTIFY_BADWORDS - , QUERY_MOVE - , QUERY_TRADE - , ASK_PASSWORD - , DLG_RETRY - , DLG_SCORES(true) - , DLG_USEDICT - , DLG_GETDICT - , GAMES_LIST_NEWGAME - , CHANGE_CONN - , GAMES_LIST_NAME_REMATCH - , ASK_DUP_PAUSE - , CHOOSE_TILES - , SHOW_TILES - , RENAME_PLAYER + NONE, + CHANGE_GROUP, + CONFIRM_CHANGE, + CONFIRM_CHANGE_PLAY, + CONFIRM_THEN, + DIALOG_NOTAGAIN, + DIALOG_OKONLY, + DIALOG_ENABLESMS, + DICT_OR_DECLINE, + DLG_CONNSTAT, + DLG_DELETED, + DLG_INVITE(true), + DLG_OKONLY, + ENABLE_NFC, + FORCE_REMOTE, + GET_NAME, + GET_NUMBER, + INVITE_CHOICES_THEN, + MOVE_DICT, + NAME_GAME, + NEW_GROUP, + PLAYER_EDIT, + ENABLE_SMS, + QUERY_ENDGAME, + RENAME_GAME, + RENAME_GROUP, + REVERT_ALL, + REVERT_COLORS, + SET_DEFAULT, + SHOW_SUBST, + WARN_NODICT_GENERIC, // the general trying-to-open case + WARN_NODICT_INVITED, // when responding to invitation + WARN_NODICT_SUBST, // when a substitution will be possible/suggested + DLG_BADWORDS, + NOTIFY_BADWORDS, + QUERY_MOVE, + QUERY_TRADE, + ASK_PASSWORD, + DLG_RETRY, + DLG_SCORES(true), + DLG_USEDICT, + DLG_GETDICT, + GAMES_LIST_NEWGAME, + CHANGE_CONN, + GAMES_LIST_NAME_REMATCH, + ASK_DUP_PAUSE, + CHOOSE_TILES, + SHOW_TILES, + RENAME_PLAYER, + BACKUP_LOADSTORE, ; private boolean m_addToStack; 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 6762dc7ba..83a4964d3 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 @@ -47,15 +47,17 @@ import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import com.jakewharton.processphoenix.ProcessPhoenix; + import org.eehouse.android.xw4.DBUtils.GameChangeType; import org.eehouse.android.xw4.DBUtils.GameGroupInfo; -import static org.eehouse.android.xw4.DBUtils.ROWID_NOTFOUND; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.ActionPair; import org.eehouse.android.xw4.DwnldDelegate.DownloadFinishedListener; import org.eehouse.android.xw4.DwnldDelegate.OnGotLcDictListener; import org.eehouse.android.xw4.Perms23.Perm; +import org.eehouse.android.xw4.ZipUtils.SaveWhat; import org.eehouse.android.xw4.jni.CommonPrefs; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; @@ -65,6 +67,7 @@ import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.LastMoveInfo; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; +import static org.eehouse.android.xw4.DBUtils.ROWID_NOTFOUND; import java.io.File; import java.io.Serializable; @@ -739,6 +742,11 @@ public class GamesListDelegate extends ListDelegateBase } break; + case BACKUP_LOADSTORE: + Uri uri = 0 == params.length ? null : Uri.parse((String)params[0]); + dialog = mkLoadStoreDlg( uri ); + break; + case NEW_GROUP: { final Renamer namer = buildRenamer( "", R.string.newgroup_label ); lstnr = new OnClickListener() { @@ -1544,10 +1552,44 @@ public class GamesListDelegate extends ListDelegateBase return handled; } - private void startLoadOrStore( boolean isStore ) + private Dialog mkLoadStoreDlg( final Uri uri ) { + Log.d( TAG, "mkLoadStoreDlg(%s)", uri ); + final BackupConfigView view = (BackupConfigView) + LocUtils.inflate( m_activity, R.layout.backup_config_view ); + view.init( uri ); + + AlertDialog.Builder ab = makeAlertBuilder() + .setView( view ) + .setPositiveButton( view.getPosButtonTxt(), new OnClickListener() { + @Override + public void onClick( DialogInterface dlg, int item ) { + if ( null == uri ) { // store case + startFileChooser( view.getSaveWhat() ); + } else { + List what = view.getSaveWhat(); + if ( ZipUtils.load( m_activity, uri, what ) ) { + ProcessPhoenix.triggerRebirth( m_activity ); + } + } + } + } ) + .setNegativeButton( android.R.string.cancel, null ) + ; + return ab.create(); + } + + // This is in liu of passing through the startActivityForResult call, + // which apparently isn't supported. + private List mSaveWhat; + + private void startFileChooser( List what ) + { + mSaveWhat = what; // will be null in load case + String intentAction = null; RequestCode rq = null; + boolean isStore = null != what; if ( isStore ) { intentAction = Intent.ACTION_CREATE_DOCUMENT; rq = RequestCode.STORE_DATA_FILE; @@ -1557,30 +1599,13 @@ public class GamesListDelegate extends ListDelegateBase } Intent intent = new Intent( intentAction ); intent.addCategory( Intent.CATEGORY_OPENABLE ); - intent.setType( "application/octet-stream" ); + intent.setType( ZipUtils.getMimeType() ); 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 ) { @@ -1625,9 +1650,23 @@ public class GamesListDelegate extends ListDelegateBase 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 ); + boolean isStore = RequestCode.STORE_DATA_FILE == requestCode; + if ( isStore ) { + boolean saved = + ZipUtils.save( m_activity, uri, mSaveWhat ); + int msgID = saved ? R.string.db_store_done + : R.string.db_store_failed; + showToast( msgID ); + } else { + final String uriStr = uri.toString(); + post( new Runnable() { + @Override + public void run() { + showDialogFragment( DlgID.BACKUP_LOADSTORE, uriStr ); + } + } ); + } } break; } @@ -1843,9 +1882,18 @@ public class GamesListDelegate extends ListDelegateBase Utils.emailAuthor( m_activity ); break; - case R.id.games_menu_loaddb: + // Load and store both use the same dialog, and both use the + // ContentResolver/startActivityForResult grossness, but in + // different orders. Store needs to know *what* to store before + // asking where, but load needs to know what's being loaded before + // asking what subset of that the user wants to use. So we start + // with the choose-what alert in the store case, and with the + // choose-where (OS) grossness in the load case case R.id.games_menu_storedb: - startLoadOrStore( R.id.games_menu_storedb == itemID ); + showDialogFragment( DlgID.BACKUP_LOADSTORE ); + break; + case R.id.games_menu_loaddb: + startFileChooser( null ); break; case R.id.games_menu_writegit: diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java index 717395d03..67fc42a60 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java @@ -40,6 +40,7 @@ import org.eehouse.android.xw4.jni.CommonPrefs; import org.eehouse.android.xw4.loc.LocUtils; import java.io.File; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; @@ -367,21 +368,20 @@ public class PrefsDelegate extends DelegateBase } } - static void savePrefs( Context context ) + static Serializable getPrefs( Context context ) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( context ); Map all = prefs.getAll(); - HashMap copy = new HashMap<>(); + HashMap result = new HashMap<>(); for ( String key : all.keySet() ) { - copy.put( key, all.get(key) ); + result.put( key, all.get(key) ); } - DBUtils.setSerializableFor( context, PREFS_KEY, copy ); + return result; } - static void loadPrefs( Context context ) { - HashMap map = (HashMap)DBUtils - .getSerializableFor( context, PREFS_KEY ); - if ( null != map ) { + static void loadPrefs( Context context, Serializable obj ) { + if ( null != obj ) { + HashMap map = (HashMap)obj; SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences( context ) .edit(); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java index abd830adb..a8419b256 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java @@ -788,29 +788,35 @@ public class Utils { return Base64.decode( in, Base64.NO_WRAP ); } - public static Object string64ToSerializable( String str64 ) + public static Serializable bytesToSerializable( byte[] bytes ) { - Object result = null; - byte[] bytes = base64Decode( str64 ); + Serializable result = null; try { ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(bytes) ); - result = ois.readObject(); + result = (Serializable)ois.readObject(); } catch ( Exception ex ) { Log.d( TAG, "%s", ex.getMessage() ); } return result; } - public static String serializableToString64( Serializable obj ) + public static Object string64ToSerializable( String str64 ) { - String result = null; + byte[] bytes = base64Decode( str64 ); + Serializable result = bytesToSerializable(bytes); + return result; + } + + public static byte[] serializableToBytes( Serializable obj ) + { + byte[] result = null; ByteArrayOutputStream bas = new ByteArrayOutputStream(); try { ObjectOutputStream out = new ObjectOutputStream( bas ); out.writeObject( obj ); out.flush(); - result = base64Encode( bas.toByteArray() ); + result = bas.toByteArray(); } catch ( Exception ex ) { Log.ex( TAG, ex ); Assert.failDbg(); @@ -818,6 +824,13 @@ public class Utils { return result; } + public static String serializableToString64( Serializable obj ) + { + byte[] asBytes = serializableToBytes( obj ); + String result = base64Encode( asBytes ); + return result; + } + public static void testSerialization( Serializable obj ) { if ( false && BuildConfig.DEBUG ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ZipUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ZipUtils.java new file mode 100644 index 000000000..08748b527 --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ZipUtils.java @@ -0,0 +1,218 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2022 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.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class ZipUtils { + private static final String TAG = ZipUtils.class.getSimpleName(); + + public static enum SaveWhat { // COLORS, + SETTINGS, + GAMES, + ; + + String entryName() { return toString(); } + }; + + static String getMimeType() { + return "application/x-zip"; + // return "application/octet-stream"; + } + + private interface EntryIter { + boolean withEntry( ZipInputStream zis, SaveWhat what ) throws FileNotFoundException, IOException; + } + + public static List getHasWhats( Context context, Uri uri ) + { + final List result = new ArrayList<>(); + try { + iterate( context, uri, new EntryIter() { + @Override + public boolean withEntry( ZipInputStream zis, SaveWhat what ) { + result.add( what ); + return true; + } + } ); + } catch ( IOException ioe ) { + Log.ex( TAG, ioe ); + } + Log.d( TAG, "getHasWhats() => %s", result ); + return result; + } + + public static boolean load( Context context, Uri uri, + final List whats ) + { + Log.d( TAG, "load(%s)", whats ); + boolean result = false; + try { + result = iterate( context, uri, new EntryIter() { + @Override + public boolean withEntry( ZipInputStream zis, SaveWhat what ) + throws FileNotFoundException, IOException { + boolean success = false; + if ( whats.contains( what ) ) { + switch ( what ) { + case SETTINGS: + success = loadSettings( context, zis ); + break; + case GAMES: + success = loadGames( context, zis ); + break; + default: + Assert.failDbg(); + break; + } + } + return success; + } + } ); + } catch ( Exception ex ) { + Log.ex( TAG, ex ); + result = false; + } + + return result; + } + + private static boolean iterate( Context context, Uri uri, EntryIter iter ) + throws IOException, FileNotFoundException + { + boolean success = true; + try ( InputStream is = context + .getContentResolver().openInputStream( uri ) ) { + ZipInputStream zis = new ZipInputStream( is ); + while ( success ) { + ZipEntry ze = zis.getNextEntry(); + if ( null == ze ) { + break; + } + String name = ze.getName(); + Log.d( TAG, "next entry name: %s", name ); + SaveWhat what = SaveWhat.valueOf( name ); + success = iter.withEntry( zis, what ); + } + zis.close(); + } + return success; + } + + public static boolean save( Context context, Uri uri, + List whats ) + { + Log.d( TAG, "save(%s)", whats ); + boolean success = false; + ContentResolver resolver = context.getContentResolver(); + // resolver.delete( uri, null, null ); // nuke the file if exists + try ( OutputStream os = resolver.openOutputStream( uri ) ) { + ZipOutputStream zos = new ZipOutputStream( os ) ; + + for ( SaveWhat what : whats ) { + zos.putNextEntry( new ZipEntry( what.entryName() ) ); + switch ( what ) { + // case SAVE_COLORS: + // success = saveColors( zos, ze ); + // break; + case SETTINGS: + success = saveSettings( context, zos ); + break; + case GAMES: + success = saveGames( context, zos ); + break; + default: + Assert.failDbg(); + } + if ( success ) { + zos.closeEntry(); + } else { + break; + } + } + zos.close(); + os.close(); + } catch ( Exception ex ) { + Log.ex( TAG, ex ); + } + Log.d( TAG, "save(%s) DONE", whats ); + return success; + } + + private static boolean saveGames( Context context, ZipOutputStream zos ) + throws FileNotFoundException, IOException + { + String name = DBHelper.getDBName(); + File gamesDB = context.getDatabasePath( name ); + FileInputStream fis = new FileInputStream( gamesDB ); + boolean success = DBUtils.copyStream( zos, fis ); + return success; + } + + private static boolean loadGames( Context context, ZipInputStream zis ) + throws FileNotFoundException, IOException + { + String name = DBHelper.getDBName(); + File gamesDB = context.getDatabasePath( name ); + FileOutputStream fos = new FileOutputStream( gamesDB ); + boolean success = DBUtils.copyStream( fos, zis ); + return success; + } + + private static boolean saveSettings( Context context, ZipOutputStream zos ) + throws IOException + { + Serializable map = PrefsDelegate.getPrefs( context ); + byte[] asBytes = Utils.serializableToBytes( map ); + ByteArrayInputStream bis = new ByteArrayInputStream( asBytes ); + boolean success = DBUtils.copyStream( zos, bis ); + return success; + } + + private static boolean loadSettings( Context context, ZipInputStream zis ) + { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + boolean success = DBUtils.copyStream( bos, zis ); + if ( success ) { + Serializable map = Utils.bytesToSerializable( bos.toByteArray() ); + PrefsDelegate.loadPrefs( context, map ); + } + return success; + } +} diff --git a/xwords4/android/app/src/main/res/layout/backup_config_view.xml b/xwords4/android/app/src/main/res/layout/backup_config_view.xml new file mode 100644 index 000000000..75df88a94 --- /dev/null +++ b/xwords4/android/app/src/main/res/layout/backup_config_view.xml @@ -0,0 +1,25 @@ + + + + + + + + + +