package org.deviceconnect.android.deviceplugin.host.file; import android.Manifest; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import org.deviceconnect.android.activity.PermissionUtility; import org.deviceconnect.android.provider.FileManager; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * ファイル操作を行うクラス. */ public class FileDataManager { /** バッファーサイズ. */ private static final int BUFFER_SIZE = 1024; /** * ファイル更新チェックの間隔を定義する. * <p> * ここの時間を短くすることで監視時間が早まる。 */ private static final int PERIOD = 10; /** * ファイル更新タイマー. */ private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor(); /** * ファイル更新タイマーキャンセル用Future. */ private ScheduledFuture<?> mFuture; /** * 前回更新確認した時間. */ private long mLastModifiedDate; /** * ファイルの更新通知用リスナー. */ private FileModifiedListener mModifiedListener; /** * ファイルマネージャー. */ private FileManager mFileManager; /** * 開いたファイルを保持するマップ. */ private Map<String, FileData> mFiles = new HashMap<String, FileData>(); /** * コンストラクタ. * * @param mgr ファイルマネージャー */ public FileDataManager(final FileManager mgr) { mFileManager = mgr; } /** * 後始末を行う. */ public void clear() { } /** * パスを変換する. * * @param file パス変換するファイル * @return ファイル名 */ public String getPath(final File file) { File mBaseDir = mFileManager.getBasePath(); String path = file.getAbsolutePath(); String base = mBaseDir.getAbsolutePath(); if (path.startsWith(base)) { return path.substring(base.length() + 1); } return null; } /** * ファイルを開く. * * @param path 開くファイルのパス * @param flag フラグ * @return FileDataオブジェクト * @throws IOException ファイルのオープンに失敗した場合に発生 */ public FileData openFileData(final String path, final FileData.Flag flag) throws IOException { if (mFiles.containsKey(path)) { throw new IllegalStateException("file is already open."); } FileData file = null; switch (flag) { case R: file = openReadFileData(path, flag); break; case RW: file = openReadWriteFileData(path, flag); break; default: break; } if (file != null) { mFiles.put(path, file); } return file; } /** * ファイルを閉じる. * * @param path 閉じるファイルのパス * @return 閉じるのに成功した場合はtrue、それ以外はfalse */ public boolean closeFileData(final String path) { FileData file = mFiles.remove(path); return file != null; } /** * 指定されたFileデータを取得する. * <p> * 指定されたパスのファイルが存在しない場合にはnullを返却する. * * @param path パス * @return FileDataオブジェクト */ public FileData getFileData(final String path) { return mFiles.get(path); } /** * ファイルを読み込む. * * @param file 読み込むファイル * @param position 読み込む位置 * @param length 読み込む長さ * @return 読み込んだデータ */ public void readFile(@NonNull final FileData file, final int position, final int length, @NonNull final ReadFileCallback callback) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Context context = mFileManager.getContext(); PermissionUtility.requestPermissions(context, new Handler(Looper.getMainLooper()), new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, new PermissionUtility.PermissionRequestCallback() { @Override public void onSuccess() { readFileInternal(file, position, length, callback); } @Override public void onFail(@NonNull String deniedPermission) { callback.onFail(); } }); } else { readFileInternal(file, position, length, callback); } } /** * @see FileDataManager#readFile(FileData, int, int, ReadFileCallback) */ private void readFileInternal(@NonNull FileData file, int position, int length, @NonNull ReadFileCallback callback) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileInputStream fis = null; try { byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; int len = 0; fis = new FileInputStream(file.getPath()); fis.skip(position); while (((len = fis.read(buffer)) != -1)) { if (count + len < length) { baos.write(buffer, 0, len); } else { int l = length - count; baos.write(buffer, 0, l); break; } count += len; } callback.onSuccess(new String(baos.toByteArray())); } catch (FileNotFoundException e) { callback.onFail(); } catch (IOException e) { callback.onFail(); } finally { if (fis != null) { fis.close(); } } } catch (Throwable throwable) { callback.onFail(); } } /** * ファイルにデータを書き込む. * * @param file 書き込み先のファイル * @param data 書き込むデータ * @param pos 書き込む位置 * @param callback */ public void writeFile(@NonNull final FileData file, @NonNull final byte[] data, final int pos, @NonNull final WriteFileCallback callback) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Context context = mFileManager.getContext(); PermissionUtility.requestPermissions(context, new Handler(Looper.getMainLooper()), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, new PermissionUtility.PermissionRequestCallback() { @Override public void onSuccess() { writeFileInternal(file, data, pos, callback); } @Override public void onFail(@NonNull String deniedPermission) { callback.onFail(); } }); } else { writeFileInternal(file, data, pos, callback); } } /** * ファイルにデータを書き込む. * * @param file 書き込み先のファイル * @param data 書き込むデータ * @param pos 書き込む位置 * @param callback * @see FileDataManager#writeFile(FileData, byte[], int, WriteFileCallback) */ private void writeFileInternal(@NonNull FileData file, @NonNull byte[] data, int pos, @NonNull WriteFileCallback callback) { try { FileOutputStream fos = null; try { fos = new FileOutputStream(file.getPath()); fos.write(data, pos, data.length - pos); } catch (FileNotFoundException e) { callback.onFail(); return; } catch (IOException e) { callback.onFail(); return; } finally { if (fos != null) { fos.close(); } } callback.onSuccess(); } catch (Throwable throwable) { callback.onFail(); } } /** * Readモードでファイルを開く. * * @param path パス * @param flag フラグ * @return FileDataオブジェクト * @throws FileNotFoundException ファイルが見つからない場合に発生する */ private FileData openReadFileData(final String path, final FileData.Flag flag) throws FileNotFoundException { File mBaseDir = mFileManager.getBasePath(); String tmpPath = path; if (!tmpPath.startsWith("/")) { tmpPath = "/" + path; } FileData file = new FileData(); File f = new File(mBaseDir + tmpPath); file.setFile(f); file.setFlag(flag); return file; } /** * Read/Writeモードでファイルを開く. * * @param path パス * @param flag フラグ * @return FileDataオブジェクト * @throws FileNotFoundException ファイルが見つからない場合に発生する */ private FileData openReadWriteFileData(final String path, final FileData.Flag flag) throws FileNotFoundException { File mBaseDir = mFileManager.getBasePath(); String tmpPath = path; if (!tmpPath.startsWith("/")) { tmpPath = "/" + path; } FileData file = new FileData(); File f = new File(mBaseDir + tmpPath); file.setFile(f); file.setFlag(flag); return file; } /** * ファイル監視タイマーを開始する. */ public void startTimer() { if (mFuture != null) { return; } mFileManager.checkReadPermission(new FileManager.CheckPermissionCallback() { @Override public void onSuccess() { mLastModifiedDate = System.currentTimeMillis(); mFuture = mExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { getUpdatedFiles(new CheckUpdatedFilesCallback() { @Override public void onSuccess(@NonNull List<File> files) { if (files.size() > 0) { if (mModifiedListener != null) { mModifiedListener.onWatchFile(files); } } } @Override public void onFail() { } }); } }, PERIOD, PERIOD, TimeUnit.SECONDS); } @Override public void onFail() { } }); } /** * ファイル監視タイマーを停止する. */ public void stopTimer() { if (mFuture != null) { mFuture.cancel(true); mFuture = null; } } /** * ファイルの更新チェックを行う. * * @param callback 更新チェックの結果が返却されるコールバック */ public synchronized void getUpdatedFiles(final CheckUpdatedFilesCallback callback) { mFileManager.checkReadPermission(new FileManager.CheckPermissionCallback() { @Override public void onSuccess() { List<File> files = new ArrayList<File>(); File mBaseDir = mFileManager.getBasePath(); getUpdatedFiles(mBaseDir, files); mLastModifiedDate = System.currentTimeMillis(); callback.onSuccess(files); } @Override public void onFail() { callback.onFail(); } }); } /** * ファイルが更新されている場合には、リストに追加する. * <p> * ファイルがディレクトリの場合には、中のファイルも再起的に行う。 * * @param file 更新確認を行うファイル * @param modifyFiles 更新されたファイルを追加するリスト */ private void getUpdatedFiles(final File file, final List<File> modifyFiles) { if (file.isDirectory()) { File[] files = file.listFiles(); for (File f : files) { getUpdatedFiles(f, modifyFiles); } } // ファイルの更新時間が前回チェック時よりも新しい場合 long date = file.lastModified(); if (mLastModifiedDate < date) { modifyFiles.add(file); } } /** * ファイル更新通知リスナーを設定する. * * @param listener リスナー */ public void setFileModifiedListener(final FileModifiedListener listener) { mModifiedListener = listener; } /** * ファイルの更新通知用リスナー. */ public interface FileModifiedListener { /** * ファイルの更新が発見された場合に通知される. * * @param files 更新されたファイル */ void onWatchFile(List<File> files); } public interface ReadFileCallback { void onSuccess(@NonNull String data); void onFail(); } public interface WriteFileCallback { void onSuccess(); void onFail(); } public interface CheckUpdatedFilesCallback { void onSuccess(@NonNull List<File> files); void onFail(); } }