diff options
| author | Menny Even Danan <menny@evendanan.net> | 2020-07-07 03:31:56 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-07 03:31:56 +0000 |
| commit | 797e6b0800c5ef328e6a2bbaaec30916ee0328ff (patch) | |
| tree | 514708413f3ccbc6e3fc899705a0f25f94bfee70 | |
| parent | d7ec5c710b16d32ffabb07812437adf56df2fc3d (diff) | |
| parent | 83228913f7d5b296819325d0cd3d909cd776afd9 (diff) | |
| download | AnySoftKeyboard-797e6b0800c5ef328e6a2bbaaec30916ee0328ff.tar.gz AnySoftKeyboard-797e6b0800c5ef328e6a2bbaaec30916ee0328ff.tar.bz2 | |
Merge pull request #2365 from lubenard/PR_choose_backup_restore_file_path
Be able to choose custom path for backup and restore #1780
24 files changed, 650 insertions, 45 deletions
diff --git a/ime/app/src/main/AndroidManifest.xml b/ime/app/src/main/AndroidManifest.xml index a68ef5fb3..56f8a8a84 100644 --- a/ime/app/src/main/AndroidManifest.xml +++ b/ime/app/src/main/AndroidManifest.xml @@ -119,6 +119,21 @@ android:label="@string/ime_name" android:roundIcon="@mipmap/ic_launcher_round" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + + <!-- File explorer only when android version < 4.4 --> + <activity + android:name="com.anysoftkeyboard.ui.FileExplorerCreate" + android:icon="@mipmap/ic_launcher" + android:label="@string/ime_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.AskFileExplorer" /> + + <activity + android:name="com.anysoftkeyboard.ui.FileExplorerRestore" + android:icon="@mipmap/ic_launcher" + android:label="@string/ime_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.AskFileExplorer" /> </application> </manifest> diff --git a/ime/app/src/main/java/com/anysoftkeyboard/prefs/GlobalPrefsBackup.java b/ime/app/src/main/java/com/anysoftkeyboard/prefs/GlobalPrefsBackup.java index fde93b79f..b9e9e136d 100644 --- a/ime/app/src/main/java/com/anysoftkeyboard/prefs/GlobalPrefsBackup.java +++ b/ime/app/src/main/java/com/anysoftkeyboard/prefs/GlobalPrefsBackup.java @@ -6,6 +6,7 @@ import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; import android.support.v4.util.Pair; import android.support.v7.preference.PreferenceManager; +import com.anysoftkeyboard.base.utils.Logger; import com.anysoftkeyboard.dictionaries.ExternalDictionaryFactory; import com.anysoftkeyboard.dictionaries.prefsprovider.UserDictionaryPrefsProvider; import com.anysoftkeyboard.dictionaries.sqlite.AbbreviationsDictionary; @@ -28,6 +29,8 @@ import java.util.Map; public class GlobalPrefsBackup { @VisibleForTesting static final String GLOBAL_BACKUP_FILENAME = "AnySoftKeyboardPrefs.xml"; + private static File customFilename = null; + public static List<ProviderDetails> getAllPrefsProviders(@NonNull Context context) { return Arrays.asList( new ProviderDetails( @@ -49,6 +52,7 @@ public class GlobalPrefsBackup { } private static Boolean backupProvider(PrefsProvider provider, PrefsRoot prefsRoot) { + Logger.d("backupProvider", "BackupProvider is called"); final PrefsRoot providerRoot = provider.getPrefsRoot(); prefsRoot .createChild() @@ -106,7 +110,7 @@ public class GlobalPrefsBackup { } @NonNull - private static Observable<ProviderDetails> doIt( + public static Observable<ProviderDetails> doIt( Pair<List<ProviderDetails>, Boolean[]> enabledProviders, Function<PrefsXmlStorage, PrefsRoot> prefsRootFactory, BiConsumer<PrefsProvider, PrefsRoot> providerAction, @@ -133,8 +137,20 @@ public class GlobalPrefsBackup { prefsRoot -> prefsRootFinalizer.accept(storage, prefsRoot)); } + public static void updateCustomFilename(File filename) { + customFilename = filename; + } + public static File getBackupFile() { - return AnyApplication.getBackupFile(GLOBAL_BACKUP_FILENAME); + File tempFilename; + + if (customFilename == null) return AnyApplication.getBackupFile(GLOBAL_BACKUP_FILENAME); + else { + // We reset the customFilename + tempFilename = customFilename; + customFilename = null; + return tempFilename; + } } public static class ProviderDetails { diff --git a/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerCreate.java b/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerCreate.java new file mode 100644 index 000000000..88edf257e --- /dev/null +++ b/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerCreate.java @@ -0,0 +1,194 @@ +package com.anysoftkeyboard.ui; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Environment; +import android.support.v4.util.Pair; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import com.anysoftkeyboard.base.utils.Logger; +import com.anysoftkeyboard.prefs.GlobalPrefsBackup; +import com.anysoftkeyboard.rx.RxSchedulers; +import com.anysoftkeyboard.ui.settings.MainFragment; +import com.menny.android.anysoftkeyboard.R; +import io.reactivex.disposables.Disposable; +import java.io.File; +import net.evendanan.pixel.RxProgressDialog; + +public class FileExplorerCreate extends AppCompatActivity { + private ListView mListViewFiles; + private File mCurrentFolder; + private File mBasePath; + + public void listFile(File basePath) { + File[] files = basePath.listFiles(); + ArrayAdapter<File> adapter = + new ArrayAdapter<File>(this, R.layout.file_explorer_single_item, files); + mListViewFiles.setAdapter(adapter); + + // Set onclickListener for all element of listView + mListViewFiles.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + AdapterView<?> parent, View view, int position, long id) { + Object o = mListViewFiles.getItemAtPosition(position); + if (new File(o.toString()).isDirectory()) { + mCurrentFolder = new File(o.toString()); + setTitle(o.toString()); + listFile(mCurrentFolder); + } else if (new File(o.toString()).isFile()) + create_builder(new File(o.toString())); + } + }); + } + + @Override + public void onBackPressed() { + if (!mCurrentFolder.equals(mBasePath)) { + int sep = mCurrentFolder.toString().lastIndexOf("/"); + setTitle(mCurrentFolder.toString().substring(0, sep)); + mCurrentFolder = new File(mCurrentFolder.toString().substring(0, sep)); + listFile(mCurrentFolder); + } else finish(); + } + + public void emptyFilenameError() { + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); + alertDialogBuilder.setMessage(R.string.file_explorer_filename_empty); + alertDialogBuilder.setPositiveButton(android.R.string.ok, null); + AlertDialog dialog = alertDialogBuilder.create(); + dialog.show(); + } + + public Disposable launch_backup(String fileOutput) { + return RxProgressDialog.create( + new Pair<>(MainFragment.supportedProviders, MainFragment.checked), + this, + getText(R.string.take_a_while_progress_message), + R.layout.progress_window) + .subscribeOn(RxSchedulers.background()) + .flatMap(GlobalPrefsBackup::backup) + .observeOn(RxSchedulers.mainThread()) + .subscribe( + providerDetails -> + Logger.i( + "FileExplorerCreate", + "Finished backing up %s", + providerDetails.provider.providerId()), + e -> { + Logger.w( + "FileExplorerCreate", + e, + "Failed to do operation due to %s", + e.getMessage()); + Toast.makeText( + getApplicationContext(), + this.getString(R.string.file_explorer_backup_failed), + Toast.LENGTH_LONG) + .show(); + }, + () -> + Toast.makeText( + getApplicationContext(), + this.getString( + R.string + .file_explorer_backup_success) + + fileOutput, + Toast.LENGTH_LONG) + .show()); + } + + public void create_builder(File outputFile) { + new AlertDialog.Builder(this) + .setTitle(R.string.file_explorer_alert_title) + .setMessage(R.string.file_explorer_backup_alert_message) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + launch_backup(outputFile.toString()); + finish(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.file_explorer_create_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.file_explorer_menu_add_folder: + new File(mCurrentFolder.toString() + "/askBackup").mkdir(); + Toast.makeText( + getApplicationContext(), + "Folder askBackup has been created at " + mCurrentFolder.toString(), + Toast.LENGTH_LONG) + .show(); + listFile(mCurrentFolder); + return true; + case R.id.file_explorer_menu_refresh: + listFile(mCurrentFolder); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.file_explorer_create_main_ui); + + TextView filenameTextView = (TextView) findViewById(R.id.file_explorer_filename); + ImageButton filenameButton = (ImageButton) findViewById(R.id.file_explorer_filename_button); + mListViewFiles = (ListView) findViewById(R.id.file_explorer_list_view); + + mBasePath = Environment.getExternalStorageDirectory(); + + mCurrentFolder = mBasePath; + + setTitle(mBasePath.toString()); + + listFile(mBasePath); + + filenameButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (filenameTextView.length() > 0) { + final File fileOutput = + new File( + mCurrentFolder + + "/" + + filenameTextView.getText().toString() + + ".xml"); + + GlobalPrefsBackup.updateCustomFilename(fileOutput); + if (fileOutput.exists()) create_builder(fileOutput); + else { + launch_backup(fileOutput.toString()); + finish(); + } + } else emptyFilenameError(); + } + }); + } +} diff --git a/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerRestore.java b/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerRestore.java new file mode 100644 index 000000000..3b84cd422 --- /dev/null +++ b/ime/app/src/main/java/com/anysoftkeyboard/ui/FileExplorerRestore.java @@ -0,0 +1,133 @@ +package com.anysoftkeyboard.ui; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Environment; +import android.support.v4.util.Pair; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import com.anysoftkeyboard.base.utils.Logger; +import com.anysoftkeyboard.prefs.GlobalPrefsBackup; +import com.anysoftkeyboard.rx.RxSchedulers; +import com.anysoftkeyboard.ui.settings.MainFragment; +import com.menny.android.anysoftkeyboard.R; +import io.reactivex.disposables.Disposable; +import java.io.File; +import net.evendanan.pixel.RxProgressDialog; + +public class FileExplorerRestore extends AppCompatActivity { + private ListView mListViewFiles; + private File mBasePath; + private File mCurrentFolder; + + private Disposable launch_restore(String fileName) { + return RxProgressDialog.create( + new Pair<>(MainFragment.supportedProviders, MainFragment.checked), + this, + getText(R.string.take_a_while_progress_message), + R.layout.progress_window) + .subscribeOn(RxSchedulers.background()) + .flatMap(GlobalPrefsBackup::restore) + .observeOn(RxSchedulers.mainThread()) + .subscribe( + providerDetails -> + Logger.i( + "FileExplorerRestore", + "Finished restore up %s", + providerDetails.provider.providerId()), + e -> { + Logger.w( + "FileExplorerRestore", + e, + "Failed to do operation due to %s", + e.getMessage()); + Toast.makeText( + getApplicationContext(), + this.getString(R.string.file_explorer_restore_failed), + Toast.LENGTH_LONG) + .show(); + }, + () -> + Toast.makeText( + getApplicationContext(), + this.getString( + R.string + .file_explorer_restore_success) + + fileName, + Toast.LENGTH_LONG) + .show()); + } + + public void create_builder(File fileOutput) { + new AlertDialog.Builder(this) + .setTitle(R.string.file_explorer_alert_title) + .setMessage(R.string.file_explorer_restore_alert_message) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + GlobalPrefsBackup.updateCustomFilename(fileOutput); + launch_restore(fileOutput.toString()); + finish(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + + public void listFile(File basePath) { + File[] files = basePath.listFiles(); + ArrayAdapter<File> adapter = + new ArrayAdapter<File>(this, R.layout.file_explorer_single_item, files); + mListViewFiles.setAdapter(adapter); + + // Set onclickListener for all element of listView + mListViewFiles.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + AdapterView<?> parent, View view, int position, long id) { + Object o = mListViewFiles.getItemAtPosition(position); + if (new File(o.toString()).isDirectory()) { + mCurrentFolder = new File(o.toString()); + setTitle(o.toString()); + listFile(mCurrentFolder); + } else if (new File(o.toString()).isFile()) + create_builder(new File(o.toString())); + } + }); + } + + @Override + public void onBackPressed() { + if (!mCurrentFolder.equals(mBasePath)) { + int sep = mCurrentFolder.toString().lastIndexOf("/"); + setTitle(mCurrentFolder.toString().substring(0, sep)); + mCurrentFolder = new File(mCurrentFolder.toString().substring(0, sep)); + listFile(mCurrentFolder); + } else finish(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.file_explorer_restore_main_ui); + + mListViewFiles = (ListView) findViewById(R.id.file_explorer_list_view); + + mBasePath = Environment.getExternalStorageDirectory(); + + mCurrentFolder = mBasePath; + + setTitle(mBasePath.toString()); + + listFile(mBasePath); + } +} diff --git a/ime/app/src/main/java/com/anysoftkeyboard/ui/settings/MainFragment.java b/ime/app/src/main/java/com/anysoftkeyboard/ui/settings/MainFragment.java index 265d4d9a5..7a40a1e6d 100644 --- a/ime/app/src/main/java/com/anysoftkeyboard/ui/settings/MainFragment.java +++ b/ime/app/src/main/java/com/anysoftkeyboard/ui/settings/MainFragment.java @@ -1,8 +1,11 @@ package com.anysoftkeyboard.ui.settings; import android.Manifest; +import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; @@ -28,13 +31,17 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import android.widget.Toast; import com.anysoftkeyboard.PermissionsRequestCodes; import com.anysoftkeyboard.base.utils.Logger; import com.anysoftkeyboard.keyboards.AnyKeyboard; import com.anysoftkeyboard.keyboards.Keyboard; import com.anysoftkeyboard.keyboards.views.DemoAnyKeyboardView; import com.anysoftkeyboard.prefs.GlobalPrefsBackup; +import com.anysoftkeyboard.prefs.backup.PrefsXmlStorage; import com.anysoftkeyboard.rx.RxSchedulers; +import com.anysoftkeyboard.ui.FileExplorerCreate; +import com.anysoftkeyboard.ui.FileExplorerRestore; import com.anysoftkeyboard.ui.settings.setup.SetUpKeyboardWizardFragment; import com.anysoftkeyboard.ui.settings.setup.SetupSupport; import com.anysoftkeyboard.ui.tutorials.ChangeLogFragment; @@ -47,6 +54,9 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposables; import io.reactivex.functions.Function; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.List; import net.evendanan.chauffeur.lib.FragmentChauffeurActivity; @@ -63,12 +73,21 @@ public class MainFragment extends Fragment { static final int DIALOG_SAVE_FAILED = 11; static final int DIALOG_LOAD_SUCCESS = 20; static final int DIALOG_LOAD_FAILED = 21; + static int successDialog; + static int failedDialog; + public static List<GlobalPrefsBackup.ProviderDetails> supportedProviders; + public static Boolean[] checked; + static Function< + Pair<List<GlobalPrefsBackup.ProviderDetails>, Boolean[]>, + ObservableSource<GlobalPrefsBackup.ProviderDetails>> + action; private final boolean mTestingBuild; private AnimationDrawable mNotConfiguredAnimation = null; @NonNull private Disposable mPaletteDisposable = Disposables.empty(); private DemoAnyKeyboardView mDemoAnyKeyboardView; + public int modeBackupRestore; private GeneralDialogController mDialogController; @NonNull private CompositeDisposable mDisposable = new CompositeDisposable(); @@ -350,16 +369,15 @@ public class MainFragment extends Fragment { private void onBackupRestoreDialogRequired(AlertDialog.Builder builder, int optionId) { final int actionString; - final Function< - Pair<List<GlobalPrefsBackup.ProviderDetails>, Boolean[]>, - ObservableSource<GlobalPrefsBackup.ProviderDetails>> - action; - final int successDialog; - final int failedDialog; + final int choosePathString = R.string.word_editor_action_choose_path; + + final String actionCustomPath; + modeBackupRestore = optionId; switch (optionId) { case R.id.backup_prefs: action = GlobalPrefsBackup::backup; actionString = R.string.word_editor_action_backup_words; + actionCustomPath = Intent.ACTION_CREATE_DOCUMENT; builder.setTitle(R.string.pick_prefs_providers_to_backup); successDialog = DIALOG_SAVE_SUCCESS; failedDialog = DIALOG_SAVE_FAILED; @@ -367,6 +385,7 @@ public class MainFragment extends Fragment { case R.id.restore_prefs: action = GlobalPrefsBackup::restore; actionString = R.string.word_editor_action_restore_words; + actionCustomPath = Intent.ACTION_GET_CONTENT; builder.setTitle(R.string.pick_prefs_providers_to_restore); successDialog = DIALOG_LOAD_SUCCESS; failedDialog = DIALOG_LOAD_FAILED; @@ -376,11 +395,10 @@ public class MainFragment extends Fragment { "The option-id " + optionId + " is not supported here."); } - final List<GlobalPrefsBackup.ProviderDetails> supportedProviders = - GlobalPrefsBackup.getAllPrefsProviders(getContext()); + supportedProviders = GlobalPrefsBackup.getAllPrefsProviders(getContext()); final CharSequence[] providersTitles = new CharSequence[supportedProviders.size()]; final boolean[] initialChecked = new boolean[supportedProviders.size()]; - final Boolean[] checked = new Boolean[supportedProviders.size()]; + checked = new Boolean[supportedProviders.size()]; for (int providerIndex = 0; providerIndex < supportedProviders.size(); providerIndex++) { // starting with everything checked @@ -399,38 +417,111 @@ public class MainFragment extends Fragment { mDisposable.dispose(); mDisposable = new CompositeDisposable(); - mDisposable.add( - RxProgressDialog.create( - new Pair<>(supportedProviders, checked), - getActivity(), - getText(R.string.take_a_while_progress_message), - R.layout.progress_window) - .subscribeOn(RxSchedulers.background()) - .flatMap(action) - .observeOn(RxSchedulers.mainThread()) - .subscribe( - providerDetails -> - Logger.i( - "MainFragment", - "Finished backing up %s", - providerDetails.provider.providerId()), - e -> { - Logger.w( - "MainFragment", - e, - "Failed to do operation due to %s", - e.getMessage()); - mDialogController.showDialog( - failedDialog, e.getMessage()); - }, - () -> - mDialogController.showDialog( - successDialog, - GlobalPrefsBackup.getBackupFile() - .getAbsolutePath()))); + mDisposable.add(launchBackupRestore(0, null)); + }); + builder.setNeutralButton( + choosePathString, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Intent dataToFileChooser = new Intent(); + dataToFileChooser.setType("text/xml"); + dataToFileChooser.setAction(actionCustomPath); + dataToFileChooser.putExtra("checked", checked); + try { + startActivityForResult(dataToFileChooser, 1); + } catch (ActivityNotFoundException e) { + Logger.e(TAG, "Could not launch the custom path activity"); + Toast.makeText( + getActivity().getApplicationContext(), + R.string.toast_error_custom_path_backup, + Toast.LENGTH_LONG) + .show(); + } + + } else { + Intent intent = null; + if (optionId == R.id.backup_prefs) { + intent = new Intent(getContext(), FileExplorerCreate.class); + } else if (optionId == R.id.restore_prefs) { + intent = new Intent(getContext(), FileExplorerRestore.class); + } + startActivity(intent); + } + } }); } + private Disposable launchBackupRestore(int custom, Uri customUri) { + File filePath; + if (custom == 1) filePath = new File(customUri.getPath()); + else filePath = GlobalPrefsBackup.getBackupFile(); + + return RxProgressDialog.create( + new Pair<>(supportedProviders, checked), + getActivity(), + getText(R.string.take_a_while_progress_message), + R.layout.progress_window) + .subscribeOn(RxSchedulers.background()) + .flatMap(action) + .observeOn(RxSchedulers.mainThread()) + .subscribe( + providerDetails -> + Logger.i( + "MainFragment", + "Finished backing up %s", + providerDetails.provider.providerId()), + e -> { + Logger.w( + "MainFragment", + e, + "Failed to do operation due to %s", + e.getMessage()); + mDialogController.showDialog(failedDialog, e.getMessage()); + }, + () -> mDialogController.showDialog(successDialog, filePath)); + } + + public static void launchRestoreCustomFileData(InputStream inputStream) { + PrefsXmlStorage.prefsXmlStorageCustomPath(inputStream); + } + + public static void launchBackupCustomFileData(OutputStream outputStream) { + PrefsXmlStorage.prefsXmlBackupCustomPath(outputStream); + } + + // This function is if launched when selecting neutral button of the main Fragment + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == 1 + && resultCode == Activity.RESULT_OK + && data != null + && data.getDataString() != null) { + + ContentResolver resolver = getContext().getContentResolver(); + Logger.d(TAG, "Resolver " + resolver.getType(data.getData())); + try { + // Actually, it is not a good idea to convert URI into filepath. + // For more informations, see: + // https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html + if (modeBackupRestore == R.id.restore_prefs) { + Logger.d(TAG, "Launching Restore at uri " + data.getData()); + launchRestoreCustomFileData(resolver.openInputStream(data.getData())); + } else if (modeBackupRestore == R.id.backup_prefs) { + Logger.d(TAG, "Launching Backup at uri " + data.getData()); + launchBackupCustomFileData(resolver.openOutputStream(data.getData())); + } + launchBackupRestore(1, data.getData()); + } catch (Exception e) { + e.printStackTrace(); + Logger.d(TAG, "Error when getting inputStream on onActivityResult"); + } + } + } + private static class StoragePermissionRequest extends PermissionsRequest.PermissionsRequestBase { diff --git a/ime/app/src/main/res/drawable-hdpi/ic_menu_add.png b/ime/app/src/main/res/drawable-hdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..9375e30cd --- /dev/null +++ b/ime/app/src/main/res/drawable-hdpi/ic_menu_add.png diff --git a/ime/app/src/main/res/drawable-hdpi/ic_menu_refresh.png b/ime/app/src/main/res/drawable-hdpi/ic_menu_refresh.png Binary files differnew file mode 100644 index 000000000..7b58598fe --- /dev/null +++ b/ime/app/src/main/res/drawable-hdpi/ic_menu_refresh.png diff --git a/ime/app/src/main/res/drawable-mdpi/ic_menu_add.png b/ime/app/src/main/res/drawable-mdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..f3bf34a97 --- /dev/null +++ b/ime/app/src/main/res/drawable-mdpi/ic_menu_add.png diff --git a/ime/app/src/main/res/drawable-mdpi/ic_menu_refresh.png b/ime/app/src/main/res/drawable-mdpi/ic_menu_refresh.png Binary files differnew file mode 100644 index 000000000..4b9605ebe --- /dev/null +++ b/ime/app/src/main/res/drawable-mdpi/ic_menu_refresh.png diff --git a/ime/app/src/main/res/drawable-xhdpi/ic_menu_add.png b/ime/app/src/main/res/drawable-xhdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..fd8e0d8ca --- /dev/null +++ b/ime/app/src/main/res/drawable-xhdpi/ic_menu_add.png diff --git a/ime/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png b/ime/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png Binary files differnew file mode 100644 index 000000000..2d5addc8d --- /dev/null +++ b/ime/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png diff --git a/ime/app/src/main/res/drawable-xxhdpi/ic_menu_add.png b/ime/app/src/main/res/drawable-xxhdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..e4380b31e --- /dev/null +++ b/ime/app/src/main/res/drawable-xxhdpi/ic_menu_add.png diff --git a/ime/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png b/ime/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png Binary files differnew file mode 100644 index 000000000..8df39b56a --- /dev/null +++ b/ime/app/src/main/res/drawable-xxhdpi/ic_menu_refresh.png diff --git a/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_add.png b/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..405a672b7 --- /dev/null +++ b/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_add.png diff --git a/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh.png b/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh.png Binary files differnew file mode 100644 index 000000000..4c0269eea --- /dev/null +++ b/ime/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh.png diff --git a/ime/app/src/main/res/layout/file_explorer_create_main_ui.xml b/ime/app/src/main/res/layout/file_explorer_create_main_ui.xml new file mode 100644 index 000000000..ccd244b51 --- /dev/null +++ b/ime/app/src/main/res/layout/file_explorer_create_main_ui.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ListView + android:id="@+id/file_explorer_list_view" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1" + android:clipToPadding="false"> + </ListView> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="48dp"> + + <EditText + android:id="@+id/file_explorer_filename" + android:layout_width="250dp" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textPersonName" + android:hint="@string/file_explorer_filename" /> + + <ImageButton + android:id="@+id/file_explorer_filename_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:srcCompat="@android:drawable/ic_menu_send" /> + </LinearLayout> +</LinearLayout> diff --git a/ime/app/src/main/res/layout/file_explorer_restore_main_ui.xml b/ime/app/src/main/res/layout/file_explorer_restore_main_ui.xml new file mode 100644 index 000000000..b90c06e77 --- /dev/null +++ b/ime/app/src/main/res/layout/file_explorer_restore_main_ui.xml @@ -0,0 +1,12 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ListView + android:id="@+id/file_explorer_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false"> + </ListView> +</LinearLayout> diff --git a/ime/app/src/main/res/layout/file_explorer_single_item.xml b/ime/app/src/main/res/layout/file_explorer_single_item.xml new file mode 100644 index 000000000..7d0922da1 --- /dev/null +++ b/ime/app/src/main/res/layout/file_explorer_single_item.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/label" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:padding="10dp" + android:textSize="16dp" + android:textColor="@android:color/black"> +</TextView> diff --git a/ime/app/src/main/res/menu/file_explorer_create_menu.xml b/ime/app/src/main/res/menu/file_explorer_create_menu.xml new file mode 100644 index 000000000..03f62e65f --- /dev/null +++ b/ime/app/src/main/res/menu/file_explorer_create_menu.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/file_explorer_menu_add_folder" android:icon="@drawable/ic_menu_add" + android:title="@string/file_explorer_menu_add_folder" app:showAsAction="ifRoom|never" /> + + <item android:id="@+id/file_explorer_menu_refresh" android:icon="@drawable/ic_menu_refresh" + android:title="@string/file_explorer_menu_add_folder" app:showAsAction="ifRoom|never" /> +</menu> diff --git a/ime/app/src/main/res/values/strings.xml b/ime/app/src/main/res/values/strings.xml index 4e8fb950f..b5a83e655 100644 --- a/ime/app/src/main/res/values/strings.xml +++ b/ime/app/src/main/res/values/strings.xml @@ -735,10 +735,21 @@ <string name="enter_word_hint">Type the new word here</string> <string name="enter_abbreviation_hint">Abbreviation</string> <string name="enter_abbreviation_target_hint">Full sentence</string> + <string name="toast_error_custom_path_backup">Error: Any contextual app could be open</string> + <string name="file_explorer_menu_add_folder">Create folder</string> + <string name="file_explorer_filename">Backup filename…</string> + <string name="file_explorer_filename_empty">Filename cannot be empty…</string> + <string name="file_explorer_alert_title">Select this file ?</string> + <string name="file_explorer_restore_alert_message">Do you want to restore this file ?</string> + <string name="file_explorer_backup_alert_message">This file will be overwritten.</string> - <string name="about_additional_software_licenses">Additional Software Licenses</string> + <string name="file_explorer_backup_success">Your data have been saved to\u0020</string> + <string name="file_explorer_backup_failed">Your data have failed to ba saved.</string> + <string name="file_explorer_restore_success">Your data have been restored from\u0020</string> + <string name="file_explorer_restore_failed">Your data have failed to be restored.</string> + <string name="about_additional_software_licenses">Additional Software Licenses</string> <string name="setup_wizard_step_one_title">Enable AnySoftKeyboard</string> <string name="setup_wizard_step_one_details">In this step, you\'ll need to enable <i> @@ -902,6 +913,7 @@ <string name="prefs_providers_backed_up_to">You data was successfully backed-up to %s</string> <string name="prefs_providers_failed_restore_due_to">Failed to restore your data due to %s</string> <string name="prefs_providers_restored_to">You data was successfully restored from %s</string> + <string name="word_editor_action_choose_path">Choose custom path</string> <string name="allow_layouts_to_provide_generic_rows">Keyboards can provide generic rows</string> <string name="allow_layouts_to_provide_generic_rows_off_summary">Always use selected generic rows.</string> diff --git a/ime/app/src/main/res/values/styles.xml b/ime/app/src/main/res/values/styles.xml index 2cbdeadbe..0c457a2d0 100644 --- a/ime/app/src/main/res/values/styles.xml +++ b/ime/app/src/main/res/values/styles.xml @@ -60,6 +60,19 @@ <item name="android:textAppearanceLarge">@style/Ask.Text.Large</item> </style> + <style name="Theme.AskFileExplorer" + parent="Theme.AppCompat.Light.DarkActionBar"> + <item name="android:textColorLink">@color/text_color_link</item> + <item name="colorAccent">@color/app_accent</item> + <!-- I'm going to use the keyboard's background, so no need for double drawing --> + <item name="android:imeFullscreenBackground">@null</item> + <item name="preferenceTheme">@style/Theme.AskPrefs</item> + + <item name="colorPrimary">@color/app_color_primary</item> + <item name="colorPrimaryDark">@color/app_color_primary_dark</item> + + </style> + <style name="Theme.AskApp.NoTitle" parent="Theme.AskApp"> <item name="windowNoTitle">true</item> diff --git a/ime/app/src/test/java/com/anysoftkeyboard/prefs/GlobalPrefsBackupTest.java b/ime/app/src/test/java/com/anysoftkeyboard/prefs/GlobalPrefsBackupTest.java index 14866cd15..2fb5b5cad 100644 --- a/ime/app/src/test/java/com/anysoftkeyboard/prefs/GlobalPrefsBackupTest.java +++ b/ime/app/src/test/java/com/anysoftkeyboard/prefs/GlobalPrefsBackupTest.java @@ -2,6 +2,7 @@ package com.anysoftkeyboard.prefs; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import android.os.Environment; import android.support.annotation.Nullable; import android.support.v4.util.Pair; import com.anysoftkeyboard.AnySoftKeyboardRobolectricTestRunner; @@ -11,6 +12,7 @@ import com.anysoftkeyboard.prefs.backup.PrefsRoot; import com.anysoftkeyboard.test.TestUtils; import com.menny.android.anysoftkeyboard.AnyApplication; import com.menny.android.anysoftkeyboard.R; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -53,6 +55,43 @@ public class GlobalPrefsBackupTest { } @Test + public void testBackupRestoreCustomPath() throws Exception { + File customFile = new File(Environment.getExternalStorageDirectory() + "/testBackup.xml"); + final FakePrefsProvider fakePrefsProvider = new FakePrefsProvider("id1"); + List<GlobalPrefsBackup.ProviderDetails> fakeDetails = + Collections.singletonList( + new GlobalPrefsBackup.ProviderDetails( + fakePrefsProvider, R.string.pop_text_type_title)); + + final AtomicReference<List<GlobalPrefsBackup.ProviderDetails>> hits = + new AtomicReference<>(new ArrayList<>()); + + GlobalPrefsBackup.updateCustomFilename(customFile); + + GlobalPrefsBackup.backup(Pair.create(fakeDetails, new Boolean[] {true})) + .blockingSubscribe(p -> hits.get().add(p)); + + Assert.assertEquals(1, hits.get().size()); + Assert.assertSame(fakePrefsProvider, hits.get().get(0).provider); + + hits.get().clear(); + + Assert.assertTrue(customFile.exists()); + Assert.assertTrue(customFile.length() > 0); + + Assert.assertNull(fakePrefsProvider.storedPrefsRoot); + + GlobalPrefsBackup.updateCustomFilename(customFile); + + GlobalPrefsBackup.restore(Pair.create(fakeDetails, new Boolean[] {true})) + .blockingSubscribe(p -> hits.get().add(p)); + + Assert.assertEquals(1, hits.get().size()); + Assert.assertSame(fakePrefsProvider, hits.get().get(0).provider); + Assert.assertNotNull(fakePrefsProvider.storedPrefsRoot); + } + + @Test public void testGetAllPrefsProviders() { final List<GlobalPrefsBackup.ProviderDetails> allPrefsProviders = GlobalPrefsBackup.getAllPrefsProviders(getApplicationContext()); diff --git a/ime/base/src/main/java/com/anysoftkeyboard/utils/XmlWriter.java b/ime/base/src/main/java/com/anysoftkeyboard/utils/XmlWriter.java index 20999528b..96a36de8e 100644 --- a/ime/base/src/main/java/com/anysoftkeyboard/utils/XmlWriter.java +++ b/ime/base/src/main/java/com/anysoftkeyboard/utils/XmlWriter.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InvalidObjectException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayDeque; @@ -67,6 +68,10 @@ public class XmlWriter { true); } + public XmlWriter(OutputStream outputFileStream) throws IOException { + this(new OutputStreamWriter(outputFileStream, Charsets.UTF8), true, 0, true); + } + /** * Begin to output an entity. * diff --git a/ime/prefs/src/main/java/com/anysoftkeyboard/prefs/backup/PrefsXmlStorage.java b/ime/prefs/src/main/java/com/anysoftkeyboard/prefs/backup/PrefsXmlStorage.java index df16320fc..c4f5af55a 100644 --- a/ime/prefs/src/main/java/com/anysoftkeyboard/prefs/backup/PrefsXmlStorage.java +++ b/ime/prefs/src/main/java/com/anysoftkeyboard/prefs/backup/PrefsXmlStorage.java @@ -5,6 +5,8 @@ import com.anysoftkeyboard.utils.XmlWriter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayDeque; import java.util.Collections; import java.util.Deque; @@ -18,20 +20,36 @@ import org.xml.sax.helpers.DefaultHandler; public class PrefsXmlStorage { private final File mStorageFile; + private static InputStream mStorageFileStream; + private static OutputStream mBackupFileStream; public PrefsXmlStorage(File storageFile) { mStorageFile = storageFile; } + public static void prefsXmlStorageCustomPath(InputStream is) { + mStorageFileStream = is; + } + + public static void prefsXmlBackupCustomPath(OutputStream is) { + mBackupFileStream = is; + } + public void store(PrefsRoot prefsRoot) throws Exception { final File targetFolder = mStorageFile.getParentFile(); // parent folder may be null in case the file is on the root folder. - if (targetFolder != null && !targetFolder.exists() && !targetFolder.mkdirs()) { + if (mBackupFileStream == null + && targetFolder != null + && !targetFolder.exists() + && !targetFolder.mkdirs()) { throw new IOException("Failed to of storage folder " + targetFolder.getAbsolutePath()); } + final XmlWriter output; // https://github.com/menny/Java-very-tiny-XmlWriter/blob/master/XmlWriter.java - final XmlWriter output = new XmlWriter(mStorageFile); + if (mBackupFileStream == null) output = new XmlWriter(mStorageFile); + else output = new XmlWriter(mBackupFileStream); + try { output.writeEntity("AnySoftKeyboardPrefs") .writeAttribute("version", Integer.toString(prefsRoot.getVersion())); @@ -77,8 +95,13 @@ public class PrefsXmlStorage { SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser parser = factory.newSAXParser(); final PrefsXmlParser prefsXmlParser = new PrefsXmlParser(); - try (FileInputStream fileInputStream = new FileInputStream(mStorageFile)) { - parser.parse(fileInputStream, prefsXmlParser); + if (mStorageFileStream == null) { + try (FileInputStream fileInputStream = new FileInputStream(mStorageFile)) { + parser.parse(fileInputStream, prefsXmlParser); + } + } else { + Logger.d("PrefsXmlStorage", "Loaded settings from custom file path"); + parser.parse(mStorageFileStream, prefsXmlParser); } return prefsXmlParser.getParsedRoot(); } |
