Androidアプリ開発

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

この記事は約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での開発を始めたい方へ

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

未経験者からシステムエンジニアを目指すのに最適かと、
まずは無料相談から♪

未経験者からプログラマーを目指すのに最適かと、
まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています、
お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適かと、
まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

スポンサーリンク
msakiをフォローする
スポンサーリンク

コメント欄

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