トップ地図アプリMap > スマホとタブレットのファイル同期
前ページ

地図アプリMap: スマホとタブレットのファイル同期

はじめに

Androidタブレット HAOVM M8 plus 上の地図アプリ Map も順調に動作するようになった。

スマホ(SP:Smart Phone)とタブレット(TC:Tablet Computer)を同時に使用するにあたり、ファイル同期を図りたい。

ファイルには以下の種類がある。

  1. レンダリング用ファイル(OSM): OSM(OpenStreetMap)地図のレンダリング用ファイルはパソコンで生成し、スマホとタブレットに同じものをコピーしている。数か月~1年に一度、更新する。陸地ポリゴン(海岸線データ)は数年に一度の更新でもよい。
  2. 地図用補助ファイル(Common): 地図用アイコン、バス時刻表、公園・観光地等の案内地図ファイルなど、 スマホとタブレットに同じものを不定期にコピーする。
  3. 国土地理院地図のタイル画像ファイル(GSI): 国土地理院サイトからダウンロードする標準地図タイル画像ファイル、航空地図タイル画像ファイル、標高タイル画像ファイル(pngファイルであるが内容は標高データ)は共用しない。共用すれば、効率化が図れるが、重要度が低いため、個別に管理する。
  4. GPS軌跡データファイル、写真ファイル、ルートファイル(SP, TC): 共有して、相互に利用したい。 ルートファイルはウォーキングやハイキングルートを予め、タブレットで作成しておくもの。 GPS軌跡データには誤差が含まれるので、これを下敷きにルートを設定することにより、速度や距離の精度を上げることができる。

パソコン、タブレット、スマホ間のファイル転送は FTP を使う。スマホ、タブレットを FTPサーバーとするのは簡単であるが、 Windowsパソコンを FTPサーバー(IIS) とするのは、ちょっと面倒であるため、場合によってはレンタルサーバーを使う。 レンタルサーバーにホームページを置いており、FFFTP でミラーリングアップロードしている。

パソコン上の Common を FFFTP でレンタルサーバーにミラーリングアップロードする。 スマホとタブレットはレンタルサーバーからミラーリングダウンロードする。一手間増えるが、環境構築が簡単になる。

フォルダの見直し

これまでは Mapディレクトリ下に全てのファイルを置いていたが、その下に大分類として、OSM、Common、GSI、SP、TC フォルダを置く。

OSM の原本はパソコンに置き、ファイル数の多いものはスマホおよびタブレットにzipファイルをFTP転送して、解凍する。

GSI はスマホおよびタブレットで個別管理とする。

Common の原本はパソコンに置き、ホームページと同様に、レンタルサーバーに FFFTP を使ってミラーリングアップロードする。 スマホおよびタブレットはレンタルサーバーからミラーリングダウンロードする。

TC にはタブレットコンピュータで生成されるファイルが置かれるが、スマホはこれをミラーリングダウンロードする。

SP にはスマホで生成されるファイルが置かれるが、タブレットはこれをミラーリングダウンロードする。

以上から、スマホおよびタブレットに必要なのはミラーリングダウンロードの機能のみである。

最初のプログラム

まずは、ミラーリングダウンロードのみとする。

サーバーからファイルリストを取り込んで、同名ファイルがローカルディレクトリになければ、無条件にダウンロードする。 ローカルに既に存在する場合は、時刻を比較して、ローカルとリモート(サーバー)が同じであれば、ダウンロードしない。 異なる場合は、ダウンロードする。

再帰処理を使えば、サブディレクトリにも対応できる。

できれば、将来、アップロードにも対応したいので、アップとダウンを区別する引数を用意した。今はまだ使っていない。

enum Command { mirroring_download, mirroring_upload }

public class FTPClientThread extends Thread {
    ConstraintLayout layout;
    Command cmd;     // mirroring_download, mirroring_upload
    String dirRemote;
    String dirLocal;

    public FTPClientThread(ConstraintLayout layout, Command cmd, String remote, String local) {
        this.layout = layout;
        this.cmd = cmd;
        this.dirRemote = remote;
        this.dirLocal = local;
    }

    @Override
    public void run() {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());
        long start = System.currentTimeMillis();
        FTPClient ftpCli = connectFTP(サーバー名, ポート番号, ユーザID, パスワード);
        if (ftpCli == null) {
            return;
        }
        try {
            Path dirLocalObj = Paths.get(Map.DIR + dirLocal);
            if (!Files.exists(dirLocalObj)) {
                Files.createDirectories(dirLocalObj);
            }   // ディレクトリがなければ生成する
            if (!ftpCli.changeWorkingDirectory(dirRemote)) {
                throw new Exception("ディレクトリー移動エラー " + dirRemote +
                                " Code=" + ftpCli.getReplyCode());
            }

            HashSet<String> setRemote = new HashSet<>();
            FTPFile[] ftpFiles = ftpCli.listFiles();
            for (FTPFile file : ftpFiles) {
                String name = file.getName();
                if (name.startsWith(".")) continue;
                setRemote.add(name);
                long timeRemote = file.getTimestamp().getTimeInMillis();
                if (file.getType() == FTPFile.DIRECTORY_TYPE) {
                    FTPClientThread thread = new FTPClientThread(layout, cmd,
                            dirRemote + "/" + name, dirLocal + "/" + name);
                    thread.start();
                    continue;
                }
                File fileObj = new File(Map.DIR + dirLocal + "/" + name);
                Path pathObj = fileObj.toPath();
                if (!Files.exists(pathObj)) {
                    Files.createFile(pathObj);
                } else {
                    FileTime timeLocal = Files.getLastModifiedTime(pathObj);
                    if (timeRemote == timeLocal.toMillis()) {
                        //snackbarMake("変更なし " + name);
                        continue;
                    }
                }
                try (OutputStream outputStream
                      = new BufferedOutputStream(new FileOutputStream(fileObj))) {
                    // get the file from Ftp server and write it in outputStream.
                    ftpCli.retrieveFile(file.getName(), outputStream);
                    FileTime time = FileTime.fromMillis(timeRemote);
                    Files.setLastModifiedTime(pathObj, time);
                }
                FileTime time = FileTime.fromMillis(timeRemote);
                Files.setLastModifiedTime(pathObj, time);
            }
            disconnectFTP(ftpCli);
            File[] filesLocal = new File(Map.DIR + dirLocal).listFiles();
            if (filesLocal != null) {
                for (File file : filesLocal) {
                    String name = file.getName();
                    if (!setRemote.contains(name)) {
                        delete(new File(Map.DIR + dirLocal + "/" + name));
                    }
                }
            }
        } catch (Exception e) {
            snackbarMake(e.toString());
        }
        long elapsed = System.currentTimeMillis() - start;
        snackbarMake(dirRemote + " " + (elapsed/1000) + "秒");
    }

    void delete(File file) throws Exception {
        if (file.isFile()) {
            Files.delete(file.toPath());
        } else if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File file1 : files) {
                    delete(file1);
                }
            }
            Files.delete(file.toPath());
        }
    }

    FTPClient connectFTP(String sHost, int nPort, String sUser, String sPass) {
        try {
            FTPClient ftpCli = new FTPClient();

            int nTimeout = 60 * 1000;   //60秒
            ftpCli.setDefaultTimeout(nTimeout);
            ftpCli.setConnectTimeout(nTimeout);  //タイムアウトをキャッチするにはこれが必要

            ftpCli.connect(sHost, nPort);
            if (!FTPReply.isPositiveCompletion(ftpCli.getReplyCode())) {
                throw new Exception("FTP接続エラー Code=" + ftpCli.getReplyCode());
            }

            ftpCli.setSoTimeout(nTimeout);

            if (!ftpCli.login(sUser, sPass)) {
                throw new Exception("FTP認証エラー Code=" + ftpCli.getReplyCode());
            }

            //ファイル転送モード設定
            //ftpCli.setFileType(FTP.ASCII_FILE_TYPE);    //テキストファイルなど
            ftpCli.setFileType(FTP.BINARY_FILE_TYPE);   //画像ファイルなど

            //モード設定
            ftpCli.enterLocalPassiveMode(); //パッシブモード
            //ftpCli.enterLocalActiveMode();  //アクティブモード
            return ftpCli;
        } catch (Exception e) {
            snackbarMake(e.toString());
            return null;
        }
    }

    void disconnectFTP(FTPClient ftpCli) throws Exception {
        if (ftpCli != null && ftpCli.isConnected()) {
            ftpCli.disconnect();
        }
    }

    private void snackbarMake(String message) {
        Snackbar.make(layout, message, 10000).show();
    }
}

まず、Common のダウンロードを試みた。当面、guides(バス時刻表)、img(地図アイコン)、infos(公園案内マップなど)がこれに含まれる。

リファレンス

[1] Android Studio 2021.1.1 スマホでFTP 受信
[2] FTPのアクティブモードとパッシブモードの違いを徹底解説
[3] Java : ディレクトリを丸ごと削除
[4] Javaプログラムからファイル・ディレクトリを削除する方法【deleteメソッド】
[5] [Java] FTPに接続してファイルをダウンロード、アップロードする方法(FTPClient) Reference: https://www.nowonbun.com/188.html [明月の開発ストーリ]