From 4dee17b7de5c8f0dd8030c978f6b635fcf50c2ca Mon Sep 17 00:00:00 2001 From: pegasko <pegasko@pegasko.art> Date: Wed, 12 Jun 2024 04:45:12 +0300 Subject: [PATCH] File export / import --- Yeeemp/app/src/main/AndroidManifest.xml | 6 +- .../art/pegasko/yeeemp/impl/DBWrapper.java | 22 ++- .../art/pegasko/yeeemp/impl/DataUtils.java | 121 ++++++++++++++ .../java/art/pegasko/yeeemp/impl/Init.java | 4 + .../yeeemp/ui/activity/QueueListActivity.java | 151 ++++++++++++++++++ .../art/pegasko/yeeemp/ui/activity/Utils.java | 6 + .../main/res/menu/queue_list_toolbar_menu.xml | 10 ++ 7 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DataUtils.java create mode 100644 Yeeemp/app/src/main/res/menu/queue_list_toolbar_menu.xml diff --git a/Yeeemp/app/src/main/AndroidManifest.xml b/Yeeemp/app/src/main/AndroidManifest.xml index 9b0c1a5..c7243d7 100644 --- a/Yeeemp/app/src/main/AndroidManifest.xml +++ b/Yeeemp/app/src/main/AndroidManifest.xml @@ -1,7 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DBWrapper.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DBWrapper.java index a162e31..44f4320 100644 --- a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DBWrapper.java +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DBWrapper.java @@ -34,7 +34,7 @@ public class DBWrapper extends Wrapper { public static final boolean DEBUG = false; public DBWrapper(Context context) { - this.db = openDB(context); + this.db = openDB(context, DB_PATH); this.queueMaker = new QueueMakerImpl(this.db); this.eventMaker = new EventMakerImpl(this.db); this.tagMaker = new TagMakerImpl(this.db); @@ -127,19 +127,33 @@ public class DBWrapper extends Wrapper { } } + /** + * Get internal database path + */ + static File getDBPath(Context context) { + return new File(context.getFilesDir(), DB_PATH); + } + /** * @return opened and initialized database */ - private SQLiteDatabase openDB(Context context) { + private static SQLiteDatabase openDB(Context context, String dbPath) { if (DBWrapper.DEBUG) { try { - new File(context.getFilesDir(), DB_PATH).delete(); + new File(context.getFilesDir(), dbPath).delete(); } catch (Exception e) { Log.wtf(TAG, e); } } - SQLiteDatabase db = SQLiteDatabase.openDatabase(new File(context.getFilesDir(), DB_PATH).getPath(), null, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY); + File path = getDBPath(context); + SQLiteDatabase db; + if (!path.exists()) { + db = SQLiteDatabase.openDatabase(path.getPath(), null, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY); + } else { + db = SQLiteDatabase.openDatabase(path.getPath(), null, SQLiteDatabase.OPEN_READWRITE); + } + initDB(db); return db; } diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DataUtils.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DataUtils.java new file mode 100644 index 0000000..4d403e7 --- /dev/null +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/DataUtils.java @@ -0,0 +1,121 @@ +/** + * Copyright 2024 pegasko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package art.pegasko.yeeemp.impl; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.FileUtils; + +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.text.SimpleDateFormat; + +import art.pegasko.yeeemp.ui.activity.Utils; + +public class DataUtils { + public static final String DATE_FORMAT = "yyyy-MM-dd-HH-mm-ss"; + + public static String formatTs(long timestamp) { + SimpleDateFormat sdf = new SimpleDateFormat(DataUtils.DATE_FORMAT); + return sdf.format(timestamp); + } + + private static void copyStream(InputStream fis, OutputStream fos) throws IOException { + byte[] buffer = new byte[1024]; + int lengthRead; + while ((lengthRead = fis.read(buffer)) > 0) { + fos.write(buffer, 0, lengthRead); + fos.flush(); + } + } + + /* just copy internal db to external storage */ + public static void exportDatabase(Context context, File directory) throws Exception { + directory.mkdirs(); + + File internalFile = DBWrapper.getDBPath(context); + File externalFile = new File(directory, "export_" + formatTs(System.currentTimeMillis()) + ".db"); + FileInputStream fis = new FileInputStream(internalFile); + FileOutputStream fos = new FileOutputStream(externalFile); + + copyStream(fis, fos); + + fis.close(); + fos.close(); + } + + /* just copy internal db to external storage */ + public static void exportDatabase(Context context, Uri uri) throws Exception { + File internalFile = DBWrapper.getDBPath(context); + InputStream fis = new FileInputStream(internalFile); + OutputStream fos = context.getContentResolver().openOutputStream(uri); + + copyStream(fis, fos); + + fis.close(); + fos.close(); + } + + /* just copy external db to internal storage */ + public static void importDatabase(Context context, Uri uri) throws Exception { + File internalFile = DBWrapper.getDBPath(context); + InputStream fis = context.getContentResolver().openInputStream(uri); + OutputStream fos = new FileOutputStream(internalFile); + + copyStream(fis, fos); + + fis.close(); + fos.close(); + } + + public static void backupDatabase(Context context) throws IOException { + File internalFile = DBWrapper.getDBPath(context); + File backupFile = new File(context.getFilesDir(), "backup.db"); + + FileInputStream fis = new FileInputStream(internalFile); + FileOutputStream fos = new FileOutputStream(backupFile); + + copyStream(fis, fos); + + fis.close(); + fos.close(); + } + + public static void restoreDatabase(Context context) throws IOException { + File internalFile = DBWrapper.getDBPath(context); + File backupFile = new File(context.getFilesDir(), "backup.db"); + + FileInputStream fis = new FileInputStream(backupFile); + FileOutputStream fos = new FileOutputStream(internalFile); + + copyStream(fis, fos); + + fis.close(); + fos.close(); + } + + public static void deleteBackupDatabase(Context context) throws IOException { + File backupFile = new File(context.getFilesDir(), "backup.db"); + backupFile.delete(); + } +} diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/Init.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/Init.java index 87c7ea5..a44a35f 100644 --- a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/Init.java +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/impl/Init.java @@ -25,4 +25,8 @@ public class Init { if (Wrapper.instance() == null) Wrapper.setInstance(new DBWrapper(context)); } + + public static void reinitDB(Context context) { + Wrapper.setInstance(new DBWrapper(context)); + } } \ No newline at end of file diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/QueueListActivity.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/QueueListActivity.java index d26418e..b0115c0 100644 --- a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/QueueListActivity.java +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/QueueListActivity.java @@ -16,29 +16,53 @@ package art.pegasko.yeeemp.ui.activity; +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import com.google.android.material.snackbar.Snackbar; +import androidx.activity.result.ActivityResultLauncher; import androidx.appcompat.app.AppCompatActivity; +import android.os.Environment; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.widget.Toast; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.PermissionChecker; import androidx.navigation.ui.AppBarConfiguration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import java.io.File; +import java.security.Permission; + import art.pegasko.yeeemp.base.Queue; import art.pegasko.yeeemp.base.Wrapper; import art.pegasko.yeeemp.databinding.ActivityQueueListBinding; import art.pegasko.yeeemp.R; +import art.pegasko.yeeemp.impl.DBWrapper; +import art.pegasko.yeeemp.impl.DataUtils; import art.pegasko.yeeemp.impl.Init; public class QueueListActivity extends AppCompatActivity { public static final String TAG = QueueListActivity.class.getSimpleName(); + private static final int REQUEST_CODE_CREATE_FILE = 37; + private static final int REQUEST_CODE_OPEN_FILE = 19; + private AppBarConfiguration appBarConfiguration; private ActivityQueueListBinding binding; private RecyclerView queueList; @@ -60,6 +84,26 @@ public class QueueListActivity extends AppCompatActivity { setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); + /* Toolbar menu */ + binding.toolbar.inflateMenu(R.menu.queue_list_toolbar_menu); + binding.toolbar.setOnMenuItemClickListener((MenuItem item) -> { + if (item.getItemId() == R.id.queue_list_toolbar_menu_export) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_TITLE, "export_" + DataUtils.formatTs(System.currentTimeMillis()) + ".db"); + startActivityForResult(intent, REQUEST_CODE_CREATE_FILE); + + return true; + } else if (item.getItemId() == R.id.queue_list_toolbar_menu_import) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + startActivityForResult(intent, REQUEST_CODE_OPEN_FILE); + + return true; + } + return false; + }); + /* Queue list */ queueListAdapter = new QueueRecyclerViewAdapter(); queueList = findViewById(R.id.content_queue_list__list); @@ -86,6 +130,113 @@ public class QueueListActivity extends AppCompatActivity { updateList(); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + super.onActivityResult(requestCode, resultCode, resultData); + + if (requestCode == REQUEST_CODE_CREATE_FILE && resultCode == Activity.RESULT_OK) { + if (resultData != null) { + Uri uri = resultData.getData(); + Log.i(TAG, "Exporting file to " + uri.toString()); + + try { + DataUtils.exportDatabase(getApplicationContext(), uri); + Toast.makeText(QueueListActivity.this, "Exported database", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Log.wtf(TAG, e); + + new AlertDialog + .Builder(QueueListActivity.this) + .setTitle("Export failed") + .setMessage(e.getMessage()) + .setCancelable(true) + .setNegativeButton("OK", (DialogInterface dialog, int id) -> { dialog.cancel(); }) + .show(); + } + } + } else if (requestCode == REQUEST_CODE_OPEN_FILE && resultCode == Activity.RESULT_OK) { + if (resultData != null) { + final Uri uri = resultData.getData(); + + new AlertDialog + .Builder(QueueListActivity.this) + .setTitle("Confirm action") + .setMessage("Import file as database? This will overwrite local database with external file without checking") + .setCancelable(true) + .setPositiveButton("Yes", (DialogInterface dialog, int id) -> { + Log.i(TAG, "Importing file from " + uri.toString()); + + try { + Log.i(TAG, "Backup before restore"); + DataUtils.backupDatabase(getApplicationContext()); + + Log.i(TAG, "Importing database"); + DataUtils.importDatabase(getApplicationContext(), uri); + Toast.makeText(QueueListActivity.this, "Imported database", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Reloading database"); + Init.reinitDB(getApplicationContext()); + updateList(); + + Log.i(TAG, "Delete backup"); + DataUtils.deleteBackupDatabase(getApplicationContext()); + } catch (Exception e) { + Log.e(TAG, "Import failed"); + Log.wtf(TAG, e); + + new AlertDialog + .Builder(QueueListActivity.this) + .setTitle("Import failed") + .setMessage(e.getMessage()) + .setCancelable(true) + .setNegativeButton("OK", (DialogInterface dialog2, int id2) -> { dialog2.cancel(); }) + .show(); + + try { + Log.i(TAG, "Trying to restore backup"); + DataUtils.restoreDatabase(getApplicationContext()); + + Log.i(TAG, "Reloading database"); + Init.reinitDB(getApplicationContext()); + updateList(); + + Log.i(TAG, "Delete backup"); + DataUtils.deleteBackupDatabase(getApplicationContext()); + + new AlertDialog + .Builder(QueueListActivity.this) + .setTitle("Info") + .setMessage("Restored backup") + .setCancelable(true) + .setNegativeButton("OK", (DialogInterface dialog2, int id2) -> { dialog2.cancel(); }) + .show(); + + } catch (Exception e2) { + Log.e(TAG, "Restore backup failed"); + Log.wtf(TAG, e2); + + new AlertDialog + .Builder(QueueListActivity.this) + .setTitle("Restore backup failed") + .setMessage(e2.getMessage()) + .setCancelable(true) + .setNegativeButton("OK", (DialogInterface dialog2, int id2) -> { dialog2.cancel(); }) + .show(); + } + } + }) + .setNegativeButton("Cancel", (DialogInterface dialog, int id) -> { dialog.cancel(); }) + .show(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.queue_list_toolbar_menu, menu); + return true; + } + @Override protected void onResume() { super.onResume(); diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/Utils.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/Utils.java index 8d91f67..3663462 100644 --- a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/Utils.java +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/Utils.java @@ -49,4 +49,10 @@ public class Utils { return finalItems; } + + public static int positiveHashCode16(Object o) { + if (o == null) + return 0; + return Math.abs(o.hashCode()) & ((1 << 16) - 1); + } } diff --git a/Yeeemp/app/src/main/res/menu/queue_list_toolbar_menu.xml b/Yeeemp/app/src/main/res/menu/queue_list_toolbar_menu.xml new file mode 100644 index 0000000..261d8f1 --- /dev/null +++ b/Yeeemp/app/src/main/res/menu/queue_list_toolbar_menu.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/queue_list_toolbar_menu_export" + android:title="Export" /> + + <item + android:id="@+id/queue_list_toolbar_menu_import" + android:title="Import" /> +</menu> \ No newline at end of file