package com.seafile.seadroid2.data; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeadroidApplication; import com.seafile.seadroid2.SeafConnection; import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.AccountInfo; import com.seafile.seadroid2.crypto.Crypto; import com.seafile.seadroid2.util.Utils; import org.apache.commons.io.FileUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.DataInputStream; 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.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; public class DataManager { private static final String DEBUG_TAG = "DataManager"; private static final long SET_PASSWORD_INTERVAL = 59 * 60 * 1000; // 59 min // private static final long SET_PASSWORD_INTERVAL = 5 * 1000; // 5s // pull to refresh public static final String PULL_TO_REFRESH_LAST_TIME_FOR_REPOS_FRAGMENT = "repo fragment last update"; public static final String PULL_TO_REFRESH_LAST_TIME_FOR_STARRED_FRAGMENT = "starred fragment last update "; private static SimpleDateFormat ptrDataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static Map<String, PasswordInfo> passwords = Maps.newHashMap(); private static Map<String, Long> direntsRefreshTimeMap = Maps.newHashMap(); public static final long REFRESH_EXPIRATION_MSECS = 10 * 60 * 1000; // 10 mins public static long repoRefreshTimeStamp = 0; public static final int BUFFER_SIZE = 2 * 1024 * 1024; private SeafConnection sc; private Account account; private static DatabaseHelper dbHelper; private static final StorageManager storageManager = StorageManager.getInstance(); private List<SeafRepo> reposCache = null; public DataManager(Account act) { account = act; sc = new SeafConnection(act); dbHelper = DatabaseHelper.getDatabaseHelper(); } /** * Creates and returns a temporary file. It is guarantied that the file is unique and freshly * created. The caller has to delete that file himself. * * @return a newly created file. * @throws IOException if the file could not be created. */ public static File createTempFile() throws IOException { return File.createTempFile("file-", ".tmp", storageManager.getTempDir()); } /** * Creates and returns a temporary directory. It is guarantied that the directory is unique and * empty. The caller has to delete that directory himself. * * @return a newly created directory. * @throws IOException if the directory could not be created. */ public static File createTempDir() throws IOException { String dirName = "dir-" + UUID.randomUUID(); File dir = new File (storageManager.getTempDir(), dirName); if (dir.mkdir()) { return dir; } else { throw new IOException("Could not create temp directory"); } } public String getThumbnailLink(String repoName, String repoID, String filePath, int size) { File file = getLocalRepoFile(repoName, repoID, filePath); SeafRepo seafRepo = getCachedRepoByID(repoID); // encrypted repo doesn\`t support thumbnails if (seafRepo != null && seafRepo.encrypted) return null; // use locally cached file if available if (file.exists()) { return "file://" + file.getAbsolutePath(); } else { try { String pathEnc = URLEncoder.encode(filePath, "UTF-8"); return account.getServer() + String.format("api2/repos/%s/thumbnail/?p=%s&size=%s", repoID, pathEnc, size); } catch (UnsupportedEncodingException e) { return null; } } } public String getThumbnailLink(String repoID, String filePath, int size) { SeafRepo repo = getCachedRepoByID(repoID); if (repo != null) return getThumbnailLink(repo.getName(), repoID, filePath, size); else return null; } public AccountInfo getAccountInfo() throws SeafException, JSONException { String json = sc.getAccountInfo(); return parseAccountInfo(json); } private AccountInfo parseAccountInfo(String json) throws JSONException { JSONObject object = Utils.parseJsonObject(json); if (object == null) return null; return AccountInfo.fromJson(object, account.getServer()); } public ServerInfo getServerInfo() throws SeafException, JSONException { String json = sc.getServerInfo(); return parseServerInfo(json); } private ServerInfo parseServerInfo(String json) throws JSONException { JSONObject object = Utils.parseJsonObject(json); if (object == null) return null; return ServerInfo.fromJson(object, account.getServer()); } public Account getAccount() { return account; } private File getFileForReposCache() { String filename = "repos-" + (account.server + account.email).hashCode() + ".dat"; return new File(storageManager.getJsonCacheDir(), filename); } private File getFileForDirentCache(String dirID) { String filename = "dirent-" + dirID + ".dat"; return new File(storageManager.getJsonCacheDir() + "/" + filename); } private File getFileForBlockCache(String blockId) { String filename = "block-" + blockId + ".dat"; return new File(storageManager.getTempDir() + "/" + filename); } /** * The account directory structure of Seafile is like this: * * StorageManager.getMediaDir() * |__ foo@gmail.com (cloud.seafile.com) * |__ Photos * |__ Musics * |__ ... * |__ foo@mycompany.com (seafile.mycompany.com) * |__ Documents * |__ Manuals * |__ ... * |__ ... * * In the above directory, the user has used two accounts. * * 1. One account has email "foo@gmail.com" and server * "cloud.seafile.com". Two repos, "Photos" and "Musics", has been * viewed. * * 2. Another account has email "foo@mycompany.com", and server * "seafile.mycompany.com". Two repos, "Documents" and "Manuals", has * been viewed. */ public String getAccountDir() { String username = account.getEmail(); String server = Utils.stripSlashes(account.getServerHost()); // strip port, like :8000 in 192.168.1.116:8000 if (server.indexOf(":") != -1) server = server.substring(0, server.indexOf(':')); String p = String.format("%s (%s)", username, server); p = p.replaceAll("[^\\w\\d\\.@\\(\\) ]", "_"); String accountDir = Utils.pathJoin(storageManager.getMediaDir().getAbsolutePath(), p); return accountDir; } /** * Get the top dir of a repo. If there are multiple repos with same name, * say "ABC", their top dir would be "ABC", "ABC (1)", "ABC (2)", etc. The * mapping (repoID, dir) is stored in a database table. */ private synchronized String getRepoDir(String repoName, String repoID) throws RuntimeException { File repoDir; // Check if there is a record in database String uniqueRepoName = dbHelper.getRepoDir(account, repoID); if (uniqueRepoName != null) { // Has record in database repoDir = new File(getAccountDir(), uniqueRepoName); if (!repoDir.exists()) { if (!repoDir.mkdirs()) { throw new RuntimeException("Could not create library directory " + repoDir); } } return repoDir.getAbsolutePath(); } int i = 0; while (true) { if (i == 0) { uniqueRepoName = repoName; } else { uniqueRepoName = repoName + " (" + i + ")"; } repoDir = new File(getAccountDir(), uniqueRepoName); if (!repoDir.exists() && !dbHelper.repoDirExists(account, uniqueRepoName)) { // This repo dir does not exist yet, we can use it break; } i++; } if (!repoDir.mkdirs()) { throw new RuntimeException("Could not create repo directory " + uniqueRepoName + "Phone storage space is insufficient or too many " + uniqueRepoName + " directory in phone"); } // Save the new mapping in database dbHelper.saveRepoDirMapping(account, repoID, uniqueRepoName); return repoDir.getAbsolutePath(); } /** * Each repo is placed under [account-dir]/[repo-name]. When a * file is downloaded, it's placed in its repo, with its full path. * @param repoName * @param repoID * @param path */ public File getLocalRepoFile(String repoName, String repoID, String path) throws RuntimeException { String localPath = Utils.pathJoin(getRepoDir(repoName, repoID), path); File parentDir = new File(Utils.getParentPath(localPath)); if (!parentDir.exists()) { // TODO should check if the directory creation succeeds parentDir.mkdirs(); } return new File(localPath); } private List<SeafRepo> parseRepos(String json) { try { // may throw ClassCastException JSONArray array = Utils.parseJsonArray(json); if (array.length() == 0) return Lists.newArrayListWithCapacity(0); ArrayList<SeafRepo> repos = Lists.newArrayList(); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); SeafRepo repo = SeafRepo.fromJson(obj); if (repo != null) repos.add(repo); } return repos; } catch (JSONException e) { Log.e(DEBUG_TAG, "parse json error"); return null; } catch (Exception e) { // other exception, for example ClassCastException Log.e(DEBUG_TAG, "parseRepos exception"); return null; } } public String getBlockPathById(String blkId) { final File block = getFileForBlockCache(blkId); return block.getAbsolutePath(); } public SeafRepo getCachedRepoByID(String id) { List<SeafRepo> cachedRepos = getReposFromCache(); if (cachedRepos == null) { return null; } for (SeafRepo repo: cachedRepos) { if (repo.getID().equals(id)) { return repo; } } return null; } public List<SeafRepo> getReposFromCache() { if (reposCache != null) return reposCache; File cache = getFileForReposCache(); if (cache.exists()) { String json = Utils.readFile(cache); if (json == null) { return null; } reposCache = parseRepos(json); return reposCache; } return null; } public List<SeafRepo> getReposFromServer() throws SeafException { // First decide if use cache if (!Utils.isNetworkOn()) { throw SeafException.networkException; } String json = sc.getRepos(); //Log.d(DEBUG_TAG, "get repos from server " + json); if (json == null) return null; reposCache = parseRepos(json); try { File cache = getFileForReposCache(); Utils.writeFile(cache, json); } catch (IOException e) { Log.e(DEBUG_TAG, "Could not write repo cache to disk.", e); } return reposCache; } private void saveDirentContent(String repoID, String parentDir, String dirID, String content) { deleteOldDirentContent(repoID, parentDir); dbHelper.saveDirents(repoID, parentDir, dirID); try { File cache = getFileForDirentCache(dirID); Utils.writeFile(cache, content); } catch (IOException e) { Log.e(DEBUG_TAG, "Could not write dirent cache to disk.", e); } } /** * Clean up old dirent cache for a directory where we have received new data. * * @param repoID * @param dir */ private void deleteOldDirentContent(String repoID, String dir) { String dirID = dbHelper.getCachedDirents(repoID, dir); // identical directory content results in same dirID. So check if whether // the dirID is referenced multiple times before deleting it. if (dirID != null && dbHelper.getCachedDirentUsage(dirID) <= 1) { File file = getFileForDirentCache(dirID); file.delete(); } // and finally delete the entry in the SQL table dbHelper.removeCachedDirents(repoID, dir); } public synchronized File getFile(String repoName, String repoID, String path, ProgressMonitor monitor) throws SeafException { String cachedFileID = null; SeafCachedFile cf = getCachedFile(repoName, repoID, path); File localFile = getLocalRepoFile(repoName, repoID, path); // If local file is up to date, show it if (cf != null) { if (localFile.exists()) { cachedFileID = cf.fileID; } } Pair<String, File> ret = sc.getFile(repoID, path, localFile.getPath(), cachedFileID, monitor); String fileID = ret.first; if (fileID.equals(cachedFileID)) { // cache is valid return localFile; } else { File file = ret.second; addCachedFile(repoName, repoID, path, fileID, file); return file; } } public synchronized File getFileByBlocks(String repoName, String repoID, String path, int version, long fileSize, ProgressMonitor monitor) throws SeafException, IOException, JSONException, NoSuchAlgorithmException { String cachedFileID = null; SeafCachedFile cf = getCachedFile(repoName, repoID, path); File localFile = getLocalRepoFile(repoName, repoID, path); // If local file is up to date, show it if (cf != null) { if (localFile.exists()) { cachedFileID = cf.fileID; } } final String json = sc.getBlockDownloadList(repoID, path); JSONObject obj = new JSONObject(json); FileBlocks fileBlocks = FileBlocks.fromJson(obj); if (fileBlocks.fileID.equals(cachedFileID)) { // cache is valid Log.d(DEBUG_TAG, "cache is valid"); return localFile; } final Pair<String, String> pair = getRepoEncKey(repoID); final String encKey = pair.first; final String encIv = pair.second; if (TextUtils.isEmpty(encKey) || TextUtils.isEmpty(encIv)) { throw SeafException.decryptException; } if (fileBlocks.blocks == null) { if (!localFile.createNewFile()) { Log.w(DEBUG_TAG, "Failed to create file " + localFile.getName()); return null; } Log.d(DEBUG_TAG, String.format("addCachedFile repoName %s, repoId %s, path %s, fileId %s", repoName, repoID, path, fileBlocks.fileID)); addCachedFile(repoName, repoID, path, fileBlocks.fileID, localFile); return localFile; } for (Block blk : fileBlocks.blocks) { File tempBlock = new File(storageManager.getTempDir(), blk.blockId); final Pair<String, File> block = sc.getBlock(repoID, fileBlocks, blk.blockId, tempBlock.getPath(), fileSize, monitor); final byte[] bytes = FileUtils.readFileToByteArray(block.second); final byte[] decryptedBlock = Crypto.decrypt(bytes, encKey, encIv); FileUtils.writeByteArrayToFile(localFile, decryptedBlock, true); } Log.d(DEBUG_TAG, String.format("addCachedFile repoName %s, repoId %s, path %s, fileId %s", repoName, repoID, path, fileBlocks.fileID)); addCachedFile(repoName, repoID, path, fileBlocks.fileID, localFile); return localFile; } private List<SeafDirent> parseDirents(String json) { try { JSONArray array = Utils.parseJsonArray(json); if (array == null) return null; ArrayList<SeafDirent> dirents = Lists.newArrayList(); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); SeafDirent de = SeafDirent.fromJson(obj); if (de != null) dirents.add(de); } return dirents; } catch (JSONException e) { Log.e(DEBUG_TAG, "Could not parse cached dirent", e); return null; } } private List<SeafStarredFile> parseStarredFiles(String json) { try { JSONArray array = Utils.parseJsonArray(json); if (array == null) return null; ArrayList<SeafStarredFile> starredFiles = Lists.newArrayList(); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); SeafStarredFile sf = SeafStarredFile.fromJson(obj); if (sf != null) starredFiles.add(sf); } return starredFiles; } catch (JSONException e) { Log.e(DEBUG_TAG, "Could not parse cached starred files", e); return null; } } public List<SeafDirent> getCachedDirents(String repoID, String path) { String dirID = dbHelper.getCachedDirents(repoID, path); if (dirID == null) { return null; } File cache = getFileForDirentCache(dirID); if (!cache.exists()) { return null; } String json = Utils.readFile(cache); if (json == null) { return null; } return parseDirents(json); } /** * In four cases we need to visit the server for dirents * * 1. No cached dirents * 2. User clicks "refresh" button. * 3. Download all dirents within a folder * 4. View starred or searched files in gallery without available local cache * * In the second case, the local cache may still be valid. */ public List<SeafDirent> getDirentsFromServer(String repoID, String path) throws SeafException { // first fetch our cached dirent and read it String cachedDirID = dbHelper.getCachedDirents(repoID, path); String cachedContent = null; File cacheFile = getFileForDirentCache(cachedDirID); if (cacheFile.exists()) { cachedContent = Utils.readFile(cacheFile); } // if that didn't work, then we have no cache. if (cachedContent == null) { cachedDirID = null; } // fetch new dirents. ret.second will be null if the cache is still valid Pair<String, String> ret = sc.getDirents(repoID, path, cachedDirID); String content; if (ret.second != null) { String dirID = ret.first; content = ret.second; saveDirentContent(repoID, path, dirID, content); } else { content = cachedContent; } return parseDirents(content); } public List<SeafStarredFile> getStarredFiles() throws SeafException { String starredFiles = sc.getStarredFiles(); Log.v(DEBUG_TAG, "Save starred files: " + starredFiles); if (starredFiles == null) { return null; } dbHelper.saveCachedStarredFiles(account, starredFiles); return parseStarredFiles(starredFiles); } public List<SeafStarredFile> getCachedStarredFiles() { String starredFiles = dbHelper.getCachedStarredFiles(account); Log.v(DEBUG_TAG, "Get cached starred files: " + starredFiles); if (starredFiles == null) { return null; } return parseStarredFiles(starredFiles); } public SeafCachedFile getCachedFile(String repoName, String repoID, String path) { SeafCachedFile cf = dbHelper.getFileCacheItem(repoID, path, this); return cf; } public List<SeafCachedFile> getCachedFiles() { return dbHelper.getFileCacheItems(this); } public void addCachedFile(String repoName, String repoID, String path, String fileID, File file) { // notify Android Gallery that a new file has appeared // file does not always reside in Seadroid directory structure (e.g. camera upload) if (file.exists()) storageManager.notifyAndroidGalleryFileChange(file); SeafCachedFile item = new SeafCachedFile(); item.repoName = repoName; item.repoID = repoID; item.path = path; item.fileID = fileID; item.accountSignature = account.getSignature(); dbHelper.saveFileCacheItem(item, this); } public void removeCachedFile(SeafCachedFile cf) { // TODO should check if the file deletion succeeds cf.file.delete(); dbHelper.deleteFileCacheItem(cf); } public void setPassword(String repoID, String passwd) throws SeafException { sc.setPassword(repoID, passwd); } public void uploadFile(String repoName, String repoID, String dir, String filePath, ProgressMonitor monitor, boolean isUpdate, boolean isCopyToLocal) throws SeafException { uploadFileCommon(repoName, repoID, dir, filePath, monitor, isUpdate, isCopyToLocal); } private void uploadFileCommon(String repoName, String repoID, String dir, String filePath, ProgressMonitor monitor, boolean isUpdate, boolean isCopyToLocal) throws SeafException { String newFileID = sc.uploadFile(repoID, dir, filePath, monitor,isUpdate); if (newFileID == null || newFileID.length() == 0) { return; } File srcFile = new File(filePath); String path = Utils.pathJoin(dir, srcFile.getName()); File fileInRepo = null; try { fileInRepo = getLocalRepoFile(repoName, repoID, path); } catch (RuntimeException e) { e.printStackTrace(); new SeafException(SeafException.OTHER_EXCEPTION, e.getMessage()); } if (isCopyToLocal) { if (!isUpdate) { // Copy the uploaded file to local repo cache try { Utils.copyFile(srcFile, fileInRepo); } catch (IOException e) { return; } } } // Update file cache entry addCachedFile(repoName, repoID, path, newFileID, fileInRepo); } public void createNewRepo(String repoName, String password) throws SeafException { sc.createNewRepo(repoName, "", password); } public void createNewDir(String repoID, String parentDir, String dirName) throws SeafException { Pair<String, String> ret = sc.createNewDir(repoID, parentDir, dirName); if (ret == null) { return; } String newDirID = ret.first; String response = ret.second; // The response is the dirents of the parentDir after creating // the new dir. We save it to avoid request it again saveDirentContent(repoID, parentDir, newDirID, response); } public void createNewFile(String repoID, String parentDir, String fileName) throws SeafException { Pair<String, String> ret = sc.createNewFile(repoID, parentDir, fileName); if (ret == null) { return; } String newDirID = ret.first; String response = ret.second; // The response is the dirents of the parentDir after creating // the new file. We save it to avoid request it again saveDirentContent(repoID, parentDir, newDirID, response); } public File getLocalCachedFile(String repoName, String repoID, String filePath, String fileID) { File localFile = getLocalRepoFile(repoName, repoID, filePath); if (!localFile.exists()) { return null; } if (!Utils.isNetworkOn()) { return localFile; } SeafCachedFile cf = getCachedFile(repoName, repoID, filePath); if (cf != null && cf.fileID != null && cf.fileID.equals(fileID)) { return localFile; } else { return null; } } public void renameRepo(String repoID, String newName) throws SeafException { sc.renameRepo(repoID, newName); } public void deleteRepo(String repoID) throws SeafException { sc.deleteRepo(repoID); } public void star(String repoID, String path) throws SeafException { sc.star(repoID, path); } public void unstar(String repoID, String path) throws SeafException { sc.unstar(repoID, path); } public void rename(String repoID, String path, String newName, boolean isdir) throws SeafException { Pair<String, String> ret = sc.rename(repoID, path, newName, isdir); if (ret == null) { return; } String newDirID = ret.first; String response = ret.second; // The response is the dirents of the parentDir after renaming the // file/folder. We save it to avoid request it again. saveDirentContent(repoID, Utils.getParentPath(path), newDirID, response); // TODO: delete or rename cached files, dirent cache, etc. /* * I think it is more simple and easier if we provide a "clear cache" button in Settings, * just like what we have done with thumbnail caches. * And hopefully it already exist. * Users can manually clear them if they are boring with those temp files. */ } public boolean deleteShareLink(String token) { try { return sc.deleteShareLink(token); } catch (SeafException e) { e.printStackTrace(); return false; } } public ArrayList<SeafLink> getShareLink(String repoID, String path) { ArrayList<SeafLink> list = Lists.newArrayListWithCapacity(0); try { String json = sc.getShareLink(repoID, path); if (json != null) { JSONArray jsonArray = Utils.parseJsonArray(json); for (int i = 0; i < jsonArray.length(); i++) { JSONObject object = null; object = (JSONObject) jsonArray.get(i); SeafLink seafLink = SeafLink.fromJson(object); list.add(seafLink); } } } catch (SeafException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } return list; } public void delete(String repoID, String path, boolean isdir) throws SeafException{ Pair<String, String> ret = sc.delete(repoID, path, isdir); if (ret == null){ return; } String newDirID = ret.first; String response = ret.second; // The response is the dirents of the parentDir after deleting the // file/folder. We save it to avoid request it again saveDirentContent(repoID, Utils.getParentPath(path), newDirID, response); // TODO: isdir==true: recursively delete cached files, dirent cache, etc. /* * I think it is more simple and easier if we provide a "clear cache" button in Settings, * just like what we have done with thumbnail caches. * And hopefully it already exist. * Users can manually clear them if they are boring with those temp files. */ } public void copy(String srcRepoId, String srcDir, String srcFn, String dstRepoId, String dstDir) throws SeafException { sc.copy(srcRepoId, srcDir, srcFn, dstRepoId, dstDir); // After copying, we need to refresh the destination list getDirentsFromServer(dstRepoId, dstDir); } public void move(String srcRepoId, String srcDir, String srcFn, String dstRepoId, String dstDir, boolean batch) throws SeafException { Pair<String, String> ret = null; if (batch) { sc.move(srcRepoId, srcDir, srcFn, dstRepoId, dstDir); } else { String srcPath = Utils.pathJoin(srcDir, srcFn); ret = sc.move(srcRepoId, srcPath, dstRepoId, dstDir); } // After moving, we need to refresh the destination list getDirentsFromServer(dstRepoId, dstDir); // We also need to refresh the original list getDirentsFromServer(srcRepoId, srcDir); if (ret == null) { return; } String newDirID = ret.first; String response = ret.second; // The response is the list of dst after moving the // file/folder. We save it to avoid request it again saveDirentContent(dstRepoId, dstDir, newDirID, response); } public SeafActivities getEvents(int start) throws SeafException, JSONException { if (!Utils.isNetworkOn()) { throw SeafException.networkException; } final String json = sc.getEvents(start); if (json == null) return null; final JSONObject object = Utils.parseJsonObject(json); final int moreOffset = object.getInt("more_offset"); final boolean more = object.getBoolean("more"); final List<SeafEvent> events = parseEvents(json); return new SeafActivities(events, moreOffset, more); } public String getHistoryChanges(String repoId, String commitId) throws SeafException { return sc.getHistoryChanges(repoId, commitId); } public List<SeafEvent> parseEvents(String json) { try { // may throw ClassCastException JSONArray array = Utils.parseJsonArrayByKey(json, "events"); if (array.length() == 0) return Lists.newArrayListWithCapacity(0); ArrayList<SeafEvent> events = Lists.newArrayList(); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); SeafEvent event = SeafEvent.fromJson(obj); if (event != null) events.add(event); } return events; } catch (JSONException e) { Log.e(DEBUG_TAG, "parse json error"); return null; } catch (Exception e) { // other exception, for example ClassCastException Log.e(DEBUG_TAG, "parseEvents exception"); return null; } } public static void clearPassword() { passwords.clear(); } public void completeRemoteWipe() throws SeafException { sc.completeRemoteWipe(account.token); } private static class PasswordInfo { String password; // password or encKey long timestamp; public PasswordInfo(String password, long timestamp) { this.password = password; this.timestamp = timestamp; } } public boolean getRepoPasswordSet(String repoID) { final SeafRepo seafRepo = getCachedRepoByID(repoID); if (seafRepo != null && seafRepo.canLocalDecrypt()) { Pair<String, String> info = dbHelper.getEnckey(repoID); return info != null && !TextUtils.isEmpty(info.first) && !TextUtils.isEmpty(info.second); } PasswordInfo passwordInfo = passwords.get(repoID); if (passwordInfo == null) { return false; } if (Utils.now() - passwordInfo.timestamp > SET_PASSWORD_INTERVAL) { return false; } return true; } public void setRepoPasswordSet(String repoID, String key, String iv) { if (!TextUtils.isEmpty(repoID) && !TextUtils.isEmpty(key) && !TextUtils.isEmpty(iv)) { dbHelper.saveEncKey(key, iv, repoID); } } public void setRepoPasswordSet(String repoID, String password) { passwords.put(repoID, new PasswordInfo(password, Utils.now())); } public String getRepoPassword(String repoID) { if (repoID == null) { return null; } final SeafRepo seafRepo = getCachedRepoByID(repoID); if (seafRepo != null && seafRepo.canLocalDecrypt()) { final Pair<String, String> pair = dbHelper.getEnckey(repoID); if (pair == null) return null; else return pair.first; } PasswordInfo info = passwords.get(repoID); if (info == null) { return null; } return info.password; } private Pair<String, String> getRepoEncKey(String repoID) { if (repoID == null) { return null; } return dbHelper.getEnckey(repoID); } /** * calculate if refresh time is expired, the expiration is 10 mins */ public boolean isReposRefreshTimeout() { if (Utils.now() < repoRefreshTimeStamp + REFRESH_EXPIRATION_MSECS) { return false; } return true; } public boolean isDirentsRefreshTimeout(String repoID, String path) { if (!direntsRefreshTimeMap.containsKey(Utils.pathJoin(repoID, path))) { return true; } long lastRefreshTime = direntsRefreshTimeMap.get(Utils.pathJoin(repoID, path)); if (Utils.now() < lastRefreshTime + REFRESH_EXPIRATION_MSECS) { return false; } return true; } public boolean isStarredFilesRefreshTimeout() { if (!direntsRefreshTimeMap.containsKey(PULL_TO_REFRESH_LAST_TIME_FOR_STARRED_FRAGMENT)) { return true; } long lastRefreshTime = direntsRefreshTimeMap.get(PULL_TO_REFRESH_LAST_TIME_FOR_STARRED_FRAGMENT); if (Utils.now() < lastRefreshTime + REFRESH_EXPIRATION_MSECS) { return false; } return true; } public void setDirsRefreshTimeStamp(String repoID, String path) { direntsRefreshTimeMap.put(Utils.pathJoin(repoID, path), Utils.now()); } public void setReposRefreshTimeStamp() { repoRefreshTimeStamp = Utils.now(); } public void saveLastPullToRefreshTime(long lastUpdateTime, String whichFragment) { direntsRefreshTimeMap.put(whichFragment, lastUpdateTime); } public String getLastPullToRefreshTime(String whichFragment) { if (!direntsRefreshTimeMap.containsKey(whichFragment)) { return null; } Long objLastUpdate = direntsRefreshTimeMap.get(whichFragment); if (objLastUpdate == null) return null; long lastUpdate = direntsRefreshTimeMap.get(whichFragment); long diffTime = new Date().getTime() - lastUpdate; int seconds = (int) (diffTime / 1000); if (diffTime < 0) { return null; } if (seconds <= 0) { return null; } StringBuilder sb = new StringBuilder(); sb.append(SeadroidApplication.getAppContext().getString(R.string.pull_to_refresh_last_update)); if (seconds < 60) { sb.append(SeadroidApplication.getAppContext().getString(R.string.pull_to_refresh_last_update_seconds_ago, seconds)); } else { int minutes = (seconds / 60); if (minutes > 60) { int hours = minutes / 60; if (hours > 24) { Date date = new Date(lastUpdate); sb.append(ptrDataFormat.format(date)); } else { sb.append(SeadroidApplication.getAppContext().getString(R.string.pull_to_refresh_last_update_hours_ago, hours)); } } else { sb.append(SeadroidApplication.getAppContext().getString(R.string.pull_to_refresh_last_update_minutes_ago, minutes)); } } return sb.toString(); } /** * search on server * * @param query query text * @param page pass 0 to disable page loading * * @return json format strings of searched result * * @throws SeafException */ public String search(String query, int page) throws SeafException { String json = sc.searchLibraries(query, page); return json; } public ArrayList<SearchedFile> parseSearchResult(String json) { if (json == null) return null; try { JSONArray array = Utils.parseJsonArrayByKey(json, "results"); if (array == null) return null; ArrayList<SearchedFile> searchedFiles = Lists.newArrayList(); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); SearchedFile sf = SearchedFile.fromJson(obj); if (sf != null) searchedFiles.add(sf); } return searchedFiles; } catch (JSONException e) { return null; } } private FileBlocks chunkFile(String encKey, String enkIv, String filePath) throws IOException { File file = new File(filePath); InputStream in = null; DataInputStream dis; OutputStream out = null; byte[] buffer = new byte[BUFFER_SIZE]; FileBlocks seafBlock = new FileBlocks(); try { in = new FileInputStream(file); dis = new DataInputStream(in); // Log.d(DEBUG_TAG, "file size " + file.length()); int byteRead; while ((byteRead = dis.read(buffer, 0, BUFFER_SIZE)) != -1) { byte[] cipher; if (byteRead < BUFFER_SIZE) cipher = Crypto.encrypt(buffer, byteRead, encKey, enkIv); else cipher = Crypto.encrypt(buffer, encKey, enkIv); final String blkid = Crypto.sha1(cipher); File blk = new File(storageManager.getTempDir(), blkid); Block block = new Block(blkid, blk.getAbsolutePath(), blk.length(), 0L); seafBlock.blocks.add(block); out = new FileOutputStream(blk); out.write(cipher); out.close(); } in.close(); return seafBlock; } catch (FileNotFoundException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } finally { if (out != null) out.close(); if (in != null) in.close(); } } public void uploadByBlocks(String repoName, String repoId, String dir, String filePath, ProgressMonitor monitor, boolean isUpdate, boolean isCopyToLocal, int version) throws NoSuchAlgorithmException, IOException, SeafException { uploadByBlocksCommon(repoName, repoId, dir, filePath, monitor, isUpdate, isCopyToLocal, version); } private void uploadByBlocksCommon(String repoName, String repoID, String dir, String filePath, ProgressMonitor monitor, boolean isUpdate, boolean isCopyToLocal, int version) throws NoSuchAlgorithmException, IOException, SeafException { final Pair<String, String> pair = getRepoEncKey(repoID); final String encKey = pair.first; final String encIv = pair.second; // Log.d(DEBUG_TAG, "encKey " + encKey + " encIv " + encIv); if (TextUtils.isEmpty(encKey) || TextUtils.isEmpty(encIv)) { // TODO calculate them and continue throw SeafException.encryptException; } final FileBlocks chunkFile = chunkFile(encKey, encIv, filePath); if (chunkFile.blocks.isEmpty()) { throw SeafException.blockListNullPointerException; } String newFileID = sc.uploadByBlocks(repoID, dir, filePath, chunkFile.blocks, isUpdate, monitor); // Log.d(DEBUG_TAG, "uploadByBlocks " + newFileID); if (newFileID == null || newFileID.length() == 0) { return; } File srcFile = new File(filePath); String path = Utils.pathJoin(dir, srcFile.getName()); File fileInRepo = getLocalRepoFile(repoName, repoID, path); if (isCopyToLocal) { if (!isUpdate) { // Copy the uploaded file to local repo cache try { Utils.copyFile(srcFile, fileInRepo); } catch (IOException e) { return; } } } // Update file cache entry addCachedFile(repoName, repoID, path, newFileID, fileInRepo); } }