Androidアプリ開発

バックアップ(Zip圧縮)の実装

Androidアプリ開発
この記事は約23分で読めます。

こんにちは、まっさん(@Tera_Msaki)です。

この記事はAndroidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Javaでの開発経験、XML構文規則、Androidのアプリ開発経験がある方を対象としています。
Androidのアプリ開発でお役にたててれば、嬉しいです。
(これからAndroidのアプリ開発やJavaでの開発を始めたい方への案内は、記事の最後で紹介します)

冷たい飲み物も、
真空断熱マグカップだと、
蓋をしておけば、
しばらくたっても冷たいままです、、、
愛用してます

◎テーマ
複数あるファイルをZip圧縮してバックアップファイルを作成したい

◎ポイント

複数あるファイルを1ファイルにまとめることで、
ファイルのハンドリングが楽になります。
パッケージ(java.util.zip)を使用すれば、
アプリで使用しているデータファイルをZip圧縮して、
1ファイルにまとめることが可能です。

◎外部ストレージに対する権限とアクセス

Android11(APIレベル30)以降では、
外部ストレージ上のアプリ固有のディレクトリの外にあるファイルに
アクセスできなくなりました。
外部連携時の出力先フォルダとして、
ダウンロードを使用する場合は、
SAF(Storage Access Framework)を使用します。

◎Java 制御部分のコーディング(MainActivity.java)

	:
    private final Handler               handler = new Handler(Looper.getMainLooper());
	:
    //バックアップ
    private final ActivityResultLauncher<Intent> activityResultLauncher1 = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    if (result.getData() != null) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                handler.post(() -> getArchiveDatabase().dbExport(context, result.getData().getData()));
                            }
                        }).start();
                    }
                }
            });
	:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = getApplicationContext();
        setMainActivity(this);
        setArchiveDatabaseHelper(new ArchiveDatabaseHelper(context));
	:
    }
    @Override
    protected void onResume() {
        super.onResume();
        context = getApplicationContext();
	:
        //upload ボタン(バックアップ)
        upload = findViewById(R.id.upload);
        upload.setOnClickListener(v -> {
            Intent intentS = new Intent(Intent.ACTION_CREATE_DOCUMENT);
            intentS.addCategory(Intent.CATEGORY_OPENABLE);
            intentS.setType("application/zip");
            intentS.putExtra(DocumentsContract.EXTRA_INITIAL_URI, String.format("%s.zip",context.getString(R.string.app_name)));
            activityResultLauncher1.launch(intentS);
        });
	:
    //データエクスポート//
    public void dataExport(List<String> fileNames, Uri uri) {
        ExternalStorageWriter externalStorageWriter = new ExternalStorageWriter(context);
        externalStorageWriter.compress(fileNames, context, uri);
    }
    //データエクスポート完了//
    public void dataExportComplete() {
        toastMessage(R.string.data_export);
    }

バックアップボタンのクリックで、
Intentを設定して、SAFを呼び出します。
onActivityResultが非推奨で使用できなくなったので、
activityResultLauncherを使用します。
SAFからの応答より、
バックアップファイルのURIを取得します。

registerForActivityResultで取得したバックアップファイルのURIを引数として、
バックアップ処理を起動します。
バックアップ処理は時間がかかるため、
UIスレッドでタイムアウトを回避する実装が必要です。

バックアップ処理では、
データベースにあるデータをテーブル単位でファイル出力します。
出力したファイルの一覧を引数として、
Zip圧縮処理を起動します。
Zip圧縮処理の完了後にバックアップ終了メッセージをトースト表示します。

◎Java バックアップ処理のコーディング(ArchiveDatabaseHelper.java)

	:
public class ArchiveDatabaseHelper extends ArchiveUtilities {
    private static final boolean    DEBUG = false;
    private static final String     TAG = ArchiveDatabaseHelper.class.getSimpleName();
	:
    public void dbExport(Context context, Uri uri) {
        List<String> fileNames = new ArrayList<>();
        ExternalStorageWriter externalStorageWriter = new ExternalStorageWriter(context);
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            try {
                //SELECT ALL
	:
                partsList = partsDao.select();
	:
            } catch (Exception e) {
                Log.d(TAG, String.format("dbExport:%s", e.getMessage()));
                e.printStackTrace();
            }
            handler.post(() -> {
	:
                tsvFile = String.format("%s.tsv",Parts.class.getSimpleName());
                if (externalStorageWriter.initializeFile(tsvFile, 0)) {
                    for (Parts parts : partsList) {
                        externalStorageWriter.writeFile(String.format(Locale.JAPAN,"%d\t%s \t%s \t%d\n",
                                parts.getPart(), parts.getItem(), parts.getMemo(), parts.getUsage()), true);
                    }
                    fileNames.add(tsvFile);
                }
	:
                getMainActivity().dataExport(fileNames, uri);
            });
        });
    }

データベースにあるテーブルに対して、
SQLを発行してデータをファイルに出力するとともに
ファイル一覧を作成します。
データファイルの出力後、
UIスレッド経由でZip圧縮処理を起動します。

◎Java ユーティリティクラスのコーディング(ArchiveUtilities.java)

public class ArchiveUtilities extends AppCompatActivity {
    private static MainActivity           mainActivity = null;
    private static ArchiveDatabaseHelper  archiveDatabaseHelper = null;
	:
    //コンテキスト
    public void setContext(Context newContext) {
        if (context == null) {
            context = newContext;
        } else if (!context.equals(newContext)) {
            context = newContext;
        }
    }
	:
    //アクティビティ//
    public MainActivity getMainActivity() {
        return mainActivity;
    }
    public void setMainActivity(MainActivity newActivity) {
        if (maiActivity == null) {
            mainActivity = newActivity;
        } else if (!mainActivity.equals(newActivity)) {
            mainActivity = newActivity;
        }
	:
    //ArchiveDatabaseHelper//
    public ArchiveDatabaseHelper getArchiveDatabase() {
        return archiveDatabaseHelper;
    }
    public void setArchiveDatabaseHelper(ArchiveDatabaseHelper newArchiveDatabaseHelper) {
        if (archiveDatabaseHelper == null) {
            archiveDatabaseHelper = newArchiveDatabaseHelper;
        }
    }
    //トースト表示//
    public void toastMessage(int message) {
        Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
        toast.show();
    }

ユーティリティクラスは、
インスタンス化したアクティビティと
データベース操作クラスをハンドリングするクラスです。

◎Java Zip圧縮処理のコーディング(ExternalStorageWriter.java)

	:
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ExternalStorageWriter extends ArchiveUtilities {
    private static final boolean    DEBUG = false;
    private static final String     TAG = ExternalStorageWriter.class.getSimpleName();
    private final String[]          type = new String[] {
            Environment.DIRECTORY_DOCUMENTS,
            Environment.DIRECTORY_DOWNLOADS,
            Environment.DIRECTORY_PICTURES,
    };
    private final File[]            path = new File[type.length];
    private File                    file;
    private final File              propertiesFile;
    private final Handler           handler = new Handler(Looper.getMainLooper());
    private InputStream             inputStream;
    private ZipOutputStream         zipOutputStream = null;
    private File                    zipFile;
    private final byte[]            buffer = new byte[1024];

    //コンストラクタ
    public ExternalStorageWriter(Context context) {
        for (int i=0; i< type.length; i++) {
            path[i] = context.getExternalFilesDir(type[i]);
            if (DEBUG) Log.d(TAG, String.valueOf(path[i]));
        }
        propertiesFile = new File(context.getFilesDir(),String.format("%s.properties", context.getString(R.string.app_name)));
    }
    //ファイル初期化
    public boolean initializeFile(String fileName,int type) {
        if (DEBUG) Log.d(TAG, String.format("initializeFile:%s:%d",fileName,type));
        file = new File(path[type], fileName);
        return (!file.exists() || file.delete());
    }
    //ファイル書込(text)
    public void writeFile(String data, boolean mode) {
        if (isExternalStorageWritable()) {
            if (DEBUG) Log.d(TAG, String.format("writeFile:%s:%s",data,mode));
            try (FileOutputStream fileOutputStream = new FileOutputStream(file, mode);
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(outputStreamWriter)) {
                bw.write(data);
                bw.flush();
            } catch (Exception e) {
                Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                e.printStackTrace();
            }
        }
    }
	:
    //ZIP圧縮
    public void compress(List<String> inputFiles, Context context, Uri uri) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            try {
                zipOutputStream = new ZipOutputStream(
                        context.getContentResolver().openOutputStream(uri, "wt"));
            } catch (FileNotFoundException e) {
                Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                e.printStackTrace();
            }
            try {
                for (int i = 0; i < inputFiles.size(); i++) {
                    if (inputFiles.get(i).length() > 0 && !inputFiles.get(i).equals("noFile")) {
                        if (inputFiles.get(i).equals(propertiesFile.getName())) {
                            zipFile = propertiesFile;
                        } else {
                            zipFile = inputFiles.get(i).substring(inputFiles.get(i).lastIndexOf(".")).equals(".jpeg") ?
                                    new File(path[2], inputFiles.get(i)) :
                                    new File(path[0], inputFiles.get(i));
                        }
                        if (zipFile.exists()) {
                            inputStream = new FileInputStream(zipFile);
                            ZipEntry zipEntry = new ZipEntry(inputFiles.get(i));
                            Objects.requireNonNull(zipOutputStream).putNextEntry(zipEntry);
                            int length;
                            while ((length = inputStream.read(buffer)) != -1) {
                                zipOutputStream.write(buffer, 0, length);
                            }
                            inputStream.close();
                            zipOutputStream.closeEntry();
                        } else {
                            Log.d(TAG, String.format("not found:%s",inputFiles.get(i)));
                        }
                    }
                }
                if (zipOutputStream != null) zipOutputStream.close();
            } catch (IOException e) {
                Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                e.printStackTrace();
            }
            handler.post(() -> getMainActivity().dataExportComplete());
        });
    }
}

対象範囲別ストレージのため、
ファイルの拡張子で画像とそれ以外を判断して、
1ファイル毎にZip圧縮対象ファイルの読み込み先フォルダを切り替えながら、
対象ファイルをZipファイルに出力(追加)します。
Zip圧縮処理の完了後、
UIスレッドでバックアップの終了メッセージを
トースト表示します。

zipファイルを上書きで作成する場合、
openOutputStreammode は、
wtを指定します。
作成するファイルが上書きするファイルより、サイズが小さい場合に、
java.util.zip.zipexception:invalid stored block lengths のエラーが発生します。

ブログランキング・にほんブログ村へにほんブログ村

◎これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

初めてのAndroidのアプリ開発では、アプリケーション開発経験がない方や、
アプリケーション開発経験がある方でも、JavaやC#などのオブジェクト指向言語が初めての方は、
書籍などによる独学ではアプリ開発できるようになるには、
かなりの時間がかかりますので、オンラインスクールでの習得をおススメします。

コメント

タイトルとURLをコピーしました