diff --git a/.gitignore b/.gitignore index aac5a753a..59299b172 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ debugkey.properties # Misc .DS_Store app/seed.txt +app/map.txt +app/release out/ *.store diff --git a/app/build.gradle b/app/build.gradle index f7df93296..2b0db3e3d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,11 +18,6 @@ android { } } - lintOptions { - abortOnError false - disable 'MissingTranslation' - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -62,7 +57,7 @@ android { resValue "string", "account_type", "com.seafile.seadroid2.debug.account.api2" buildConfigField "String", "ACCOUNT_TYPE", '"com.seafile.seadroid2.debug.account.api2"' signingConfig signingConfigs.debug - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' applicationVariants.all { variant -> variant.outputs.all { output -> @@ -131,6 +126,16 @@ android { implementation 'org.greenrobot:eventbus:3.1.1' implementation 'com.github.bumptech.glide:glide:4.9.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' + + implementation 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.3.0' + implementation 'com.alibaba:fastjson:1.1.55.android' + } + lint { + abortOnError false + disable 'MissingTranslation' + } + buildFeatures { + viewBinding false } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 62f41589c..b98e6538a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,205 +3,224 @@ xmlns:tools="http://schemas.android.com/tools" package="com.seafile.seadroid2"> - + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - + android:theme="@style/AppTheme"> + + + + - + + - + + + + - + + + + - - + + + + - - + + - - + - + + + + + + + - - + android:label="@string/app_name" + android:screenOrientation="portrait" /> - - - - - - - - - - - - + + + + + + + + + - - - - - - - - + + + - - - - - - - - + + + - - - - - - - - - + + + + - + android:theme="@style/Theme.Fullscreen"> - + android:theme="@style/AppTheme.Editor"> + android:permission="android.permission.MANAGE_DOCUMENTS"> - - + + + - + android:label="@string/sync_provider_camera_upload" + android:syncable="true" /> + android:resource="@xml/file_paths" /> - + - - + + - + - - + + + android:permission="android.permission.BIND_JOB_SERVICE" /> @@ -242,6 +264,7 @@ + diff --git a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java index 5262f95ad..49f176cdb 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java @@ -56,8 +56,8 @@ public class SeafConnection { public static final int HTTP_STATUS_REPO_PASSWORD_REQUIRED = 440; private static final String DEBUG_TAG = "SeafConnection"; - private static final int CONNECTION_TIMEOUT = 15000; - private static final int READ_TIMEOUT = 30000; + private static final int CONNECTION_TIMEOUT = 5000000; + private static final int READ_TIMEOUT = 5000000; private Account account; diff --git a/app/src/main/java/com/seafile/seadroid2/SettingsManager.java b/app/src/main/java/com/seafile/seadroid2/SettingsManager.java index 480a890f5..002360f79 100644 --- a/app/src/main/java/com/seafile/seadroid2/SettingsManager.java +++ b/app/src/main/java/com/seafile/seadroid2/SettingsManager.java @@ -7,10 +7,14 @@ import com.seafile.seadroid2.account.AccountManager; import com.seafile.seadroid2.gesturelock.LockPatternUtils; +import com.seafile.seadroid2.loopimages.DirInfo; import com.seafile.seadroid2.util.Utils; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Access the app settings @@ -102,6 +106,10 @@ private SettingsManager() { public static final String PIC_CHECK_START = "pic_check_start"; public static final String UPLOAD_COMPLETED_TIME = "upload_completed_time"; + // Loop Images Widget + public static final String LOOPIMAGES_REMOTE_LIBRARY_DATAPLAN = PKG + ".loopimages.remote_library_data_plan_"; + public static final String LOOPIMAGES_REMOTE_LIBRARY_DIRINFO = PKG + ".loopimages.remote_library_dir_info_"; + public static long lock_timestamp = 0; public static final long LOCK_EXPIRATION_MSECS = 5 * 60 * 1000; @@ -230,6 +238,31 @@ public boolean checkCameraUploadNetworkAvailable() { return true; } + public void saveLoopImagesWidgetDataPlanAllowed(int appWidgetId, boolean isAllowed){ + settingsSharedPref.edit().putBoolean(LOOPIMAGES_REMOTE_LIBRARY_DATAPLAN + appWidgetId, isAllowed).commit(); + } + + public boolean getLoopImagesWidgetDataPlanAllowed(int appWidgetId){ + return settingsSharedPref.getBoolean(LOOPIMAGES_REMOTE_LIBRARY_DATAPLAN + appWidgetId, false); + } + + public void setLoopImagesWidgetDirInfo(int appWidgetId, List info) { + String dirInfo = TextUtils.join(";", info); + settingsSharedPref.edit().putString(LOOPIMAGES_REMOTE_LIBRARY_DIRINFO + appWidgetId, dirInfo).commit(); + } + + public List getLoopImagesWidgetDirInfo(int appWidgetId){ + String dirInfoStr = settingsSharedPref.getString(LOOPIMAGES_REMOTE_LIBRARY_DIRINFO + appWidgetId, ""); + List res = Arrays.asList(TextUtils.split(dirInfoStr, ";")); + return res; + } + + public void deleteLoopImagesWidgetInfo(int appWidgetId) { + if(settingsSharedPref.contains(LOOPIMAGES_REMOTE_LIBRARY_DIRINFO)){ + settingsSharedPref.edit().remove(LOOPIMAGES_REMOTE_LIBRARY_DIRINFO + appWidgetId).commit(); + } + } + public boolean isDataPlanAllowed() { return settingsSharedPref.getBoolean(CAMERA_UPLOAD_ALLOW_DATA_PLAN_SWITCH_KEY, false); } diff --git a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncAdapter.java b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncAdapter.java index d635705b3..6ce9c9589 100644 --- a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncAdapter.java @@ -2,12 +2,14 @@ import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RecoverableSecurityException; import android.content.AbstractThreadedSyncAdapter; import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.content.ServiceConnection; import android.content.SyncResult; import android.database.Cursor; @@ -16,10 +18,12 @@ import android.os.Bundle; import android.os.IBinder; import android.provider.MediaStore; +import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.google.common.base.Joiner; +import com.google.common.collect.Lists; import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeadroidApplication; import com.seafile.seadroid2.SeafException; @@ -28,6 +32,7 @@ import com.seafile.seadroid2.account.AccountManager; import com.seafile.seadroid2.data.CameraSyncEvent; import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.data.DirentCache; import com.seafile.seadroid2.data.SeafDirent; import com.seafile.seadroid2.data.SeafRepo; import com.seafile.seadroid2.data.StorageManager; @@ -43,10 +48,17 @@ import org.greenrobot.eventbus.EventBus; import java.io.File; +import java.io.IOException; import java.net.HttpURLConnection; +import java.text.FieldPosition; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; /** @@ -57,6 +69,7 @@ */ public class CameraSyncAdapter extends AbstractThreadedSyncAdapter { private static final String DEBUG_TAG = "CameraSyncAdapter"; + private static final String CACHE_NAME = "CameraSync"; private ContentResolver contentResolver; @@ -64,6 +77,13 @@ public class CameraSyncAdapter extends AbstractThreadedSyncAdapter { private com.seafile.seadroid2.account.AccountManager manager; private CameraUploadDBHelper dbHelper; + private MediaCursor previous = null; + private LinkedList leftBuckets; + private int synNum = 0; + + private final int leaveMeida = 2000; + private HashMap bucketMeidaNum = new HashMap(); + private String targetRepoId; private String targetRepoName; private List bucketList; @@ -121,6 +141,10 @@ public CameraSyncAdapter(Context context) { contentResolver = context.getContentResolver(); manager = new AccountManager(context); dbHelper = CameraUploadDBHelper.getInstance(); + + if(Math.random() < 0.01){ + synNum = 30; + } } private synchronized void startTransferService() { @@ -164,6 +188,11 @@ private boolean validateRepository(DataManager dataManager) throws SeafException return false; } + public void createDirectory(DataManager dataManager, String bucketName) throws SeafException { + forceCreateDirectory(dataManager, BASE_DIR,bucketName); + dataManager.getDirentsFromServer(targetRepoId, Utils.pathJoin(BASE_DIR,bucketName)); + } + /** * Create all the subdirectories on the server for the buckets that are about to be uploaded. * @@ -220,6 +249,7 @@ private void forceCreateDirectory(DataManager dataManager, String parent, String dataManager.createNewDir(targetRepoId, Utils.pathJoin("/", parent), dir); } + @RequiresApi(api = Build.VERSION_CODES.N) @Override public void onPerformSync(android.accounts.Account account, Bundle extras, String authority, @@ -229,6 +259,19 @@ public void onPerformSync(android.accounts.Account account, synchronized (this) { cancelled = false; } +// if(imageCursor != null && videoCursor != null){ +// Log.e(DEBUG_TAG, "Mixing uploading images and videos."); +// Utils.utilsLogInfo(true,"[Error] Mixing uploading images and videos."); +// imageCursor = null; +// videoCursor = null; +// } +// if(imageCursor == null && videoCursor == null) { +// bucketMeidaNum.clear(); +// Log.i(DEBUG_TAG, "Cleaning cache."); +// Utils.utilsLogInfo(true,"====Cleaning cache."); +// dbHelper.cleanRepoCache(); +// dbHelper.cleanPhotoCache(); +// } SeadroidApplication.getInstance().setScanUploadStatus(CameraSyncStatus.SCANNING); EventBus.getDefault().post(new CameraSyncEvent("start")); /*Log.i(DEBUG_TAG, "Syncing images and video to " + account); @@ -259,7 +302,6 @@ public void onPerformSync(android.accounts.Account account, Account seafileAccount = manager.getSeafileAccount(account); DataManager dataManager = new DataManager(seafileAccount); - /** * this should never occur, as camera upload is supposed to be disabled once the camera upload * account signs out. @@ -302,7 +344,7 @@ public void onPerformSync(android.accounts.Account account, // wait for TransferService to connect // Log.d(DEBUG_TAG, "waiting for transfer service"); - int timeout = 1000; // wait up to a second + int timeout = 10000; // wait up to a second while (!isCancelled() && timeout > 0 && txService == null) { // Log.d(DEBUG_TAG, "waiting for transfer service"); Thread.sleep(100); @@ -314,17 +356,13 @@ public void onPerformSync(android.accounts.Account account, syncResult.delayUntil = 60; return; } - - uploadImages(syncResult, dataManager); - - if (settingsMgr.isVideosUploadAllowed()) { - uploadVideos(syncResult, dataManager); - } + createDirectories(dataManager); + uploadBuckets(syncResult, dataManager); if (isCancelled()) { - // Log.i(DEBUG_TAG, "sync was cancelled."); + Log.i(DEBUG_TAG, "sync was cancelled."); } else { - // Log.i(DEBUG_TAG, "sync finished successfully."); + Log.i(DEBUG_TAG, "sync finished successfully."); } // Log.d(DEBUG_TAG, "syncResult: " + syncResult); @@ -351,6 +389,7 @@ public void onPerformSync(android.accounts.Account account, } } catch (Exception e) { Log.e(DEBUG_TAG, "sync aborted because an unknown error", e); + Utils.utilsLogInfo(true, "sync aborted because an unknown error: " + e.getMessage()); syncResult.stats.numParseExceptions++; } finally { if (txService != null) { @@ -368,202 +407,197 @@ public void onPerformSync(android.accounts.Account account, EventBus.getDefault().post(new CameraSyncEvent("end")); } - private void uploadImages(SyncResult syncResult, DataManager dataManager) throws SeafException, InterruptedException { - Utils.utilsLogInfo(true, "========Starting to upload images..."); - // Log.d(DEBUG_TAG, "Starting to upload images..."); - - if (isCancelled()) - return; - - List selectedBuckets = new ArrayList<>(); - if (bucketList.size() > 0) { - selectedBuckets = bucketList; - } else { - List allBuckets = GalleryBucketUtils.getMediaBuckets(SeadroidApplication.getAppContext()); - for (GalleryBucketUtils.Bucket bucket : allBuckets) { - if (bucket.isCameraBucket) - selectedBuckets.add(bucket.id); + private void uploadBuckets(SyncResult syncResult, DataManager dataManager) throws SeafException, InterruptedException{ + Utils.utilsLogInfo(true, "========Starting to upload buckets..."); + if((leftBuckets == null || leftBuckets.size() == 0) && previous == null){ + if(synNum > 30) { + Utils.utilsLogInfo(true, "========Clear photo cache..."); + dbHelper.cleanPhotoCache(); + synNum = 0; + }else { + ++synNum; } } - String[] selectionArgs = selectedBuckets.toArray(new String[]{}); - String selection = MediaStore.Images.ImageColumns.BUCKET_ID + " IN " + varArgs(selectedBuckets.size()); - - // Log.d(DEBUG_TAG, "ContentResolver selection='"+selection+"' selectionArgs='"+Arrays.deepToString(selectionArgs)+"'"); - - // fetch all new images from the ContentProvider since our last sync - Cursor cursor = contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{ - MediaStore.Images.Media._ID, - MediaStore.Images.Media.DATA, - MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME - }, - selection, - selectionArgs, - MediaStore.Images.ImageColumns.DATE_ADDED + " ASC" - ); - - try { - if (cursor == null) { - Log.e(DEBUG_TAG, "ContentResolver query failed!"); - Utils.utilsLogInfo(true,"===ContentResolver query failed!"); - return; - } - // Log.d(DEBUG_TAG, "i see " + cursor.getCount() + " new images."); - Utils.utilsLogInfo(true, "===i see " + cursor.getCount() + " images."); - if (cursor.getCount() > 0) { - // create directories for media buckets - createDirectories(dataManager); - iterateCursor(syncResult, dataManager, cursor, "images"); - - if (isCancelled()) - return; - - } - } finally { - if (cursor != null) - cursor.close(); - } - } - - private void uploadVideos(SyncResult syncResult, DataManager dataManager) throws SeafException, InterruptedException { - Utils.utilsLogInfo(true,"Starting to upload videos..."); - // Log.d(DEBUG_TAG, "Starting to upload videos..."); - - if (isCancelled()) + if (isCancelled()){ + Utils.utilsLogInfo(true, "========Cancel uploading========"); return; + } List selectedBuckets = new ArrayList<>(); - if (bucketList.size() > 0) { + if(leftBuckets != null && leftBuckets.size() > 0){ + selectedBuckets = leftBuckets; + }else if (bucketList != null && bucketList.size() > 0) { selectedBuckets = bucketList; - } else { - List allBuckets = GalleryBucketUtils.getMediaBuckets(SeadroidApplication.getAppContext()); - for (GalleryBucketUtils.Bucket bucket : allBuckets) { - if (bucket.isCameraBucket) - selectedBuckets.add(bucket.id); + } + Utils.utilsLogInfo(true, "========Traversal all phone buckets========"); + List allBuckets = GalleryBucketUtils.getMediaBuckets(SeadroidApplication.getAppContext()); + Map bucketNames = new HashMap(); + for (GalleryBucketUtils.Bucket bucket : allBuckets) { + if (bucket.isCameraBucket && bucketList.size() == 0) { + selectedBuckets.add(bucket.id); } + bucketNames.put(bucket.id, bucket.name); } - - String[] selectionArgs = selectedBuckets.toArray(new String[]{}); - String selection = MediaStore.Video.VideoColumns.BUCKET_ID + " IN " + varArgs(selectedBuckets.size()); - - // Log.d(DEBUG_TAG, "ContentResolver selection='"+selection+"' selectionArgs='"+Arrays.deepToString(selectionArgs)+"'"); - // fetch all new videos from the ContentProvider since our last sync - Cursor cursor = contentResolver.query( - MediaStore.Video.Media.EXTERNAL_CONTENT_URI, - new String[]{ - MediaStore.Video.Media._ID, - MediaStore.Video.Media.DATA, - MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME - }, - selection, - selectionArgs, - MediaStore.Video.VideoColumns.DATE_ADDED + " ASC" - ); - - try { - if (cursor == null) { - Log.e(DEBUG_TAG, "ContentResolver query failed!"); - Utils.utilsLogInfo(true,"====ContentResolver query failed!"); + if(previous != null){ + Utils.utilsLogInfo(true, "========Continue uploading previous cursor========"); + previous = iterateCursor(syncResult, dataManager, previous); + if(isCancelled()){ + Utils.utilsLogInfo(true, "========Cancel uploading========"); return; } - // Log.d(DEBUG_TAG, "i see " + cursor.getCount() + " new videos."); - Utils.utilsLogInfo(true,"=====i see " + cursor.getCount() + " videos."); - if (cursor.getCount() > 0) { - // create directories for media buckets - createDirectories(dataManager); - iterateCursor(syncResult, dataManager, cursor, "video"); + } - if (isCancelled()) - return; + Utils.utilsLogInfo(true, "========Copy select buckets========"); + leftBuckets = Lists.newLinkedList(); + leftBuckets.addAll(selectedBuckets); + Utils.utilsLogInfo(true, "========Start traversal selected buckets========"); + for(String bucketID: selectedBuckets) { + leftBuckets.removeFirst(); + String bucketName = bucketNames.get(bucketID); + if(bucketName == null || bucketName.length() == 0){ + Utils.utilsLogInfo(true, "========Bucket "+ bucketName + " does not exist."); + continue; + }else{ + Utils.utilsLogInfo(true, "========Uploading "+ bucketName+"..."); + } + String[] selectionArgs = new String[]{bucketID}; + String selection = MediaStore.Images.ImageColumns.BUCKET_ID + " = ? "; + Cursor imageCursor = contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[]{ + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATE_MODIFIED, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.DATA, + MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME + }, + selection, + selectionArgs, + MediaStore.Images.ImageColumns.DISPLAY_NAME + " ASC" + ); + Cursor videoCursor = null; + if(settingsMgr.isVideosUploadAllowed()){ + selectionArgs = new String[]{bucketID}; + selection = MediaStore.Video.VideoColumns.BUCKET_ID + " = ? "; + videoCursor = contentResolver.query( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + new String[]{ + MediaStore.Video.Media._ID, + MediaStore.Video.Media.DISPLAY_NAME, + MediaStore.Video.Media.DATE_MODIFIED, + MediaStore.Video.Media.SIZE, + MediaStore.Video.Media.DATA, + MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME + }, + selection, + selectionArgs, + MediaStore.Video.VideoColumns.DISPLAY_NAME + " ASC" + ); + } + MediaCursor cursor = new MediaCursor(bucketName, imageCursor, videoCursor); + if(cursor.getCount() > 0){ + createDirectory(dataManager, bucketName); + if (cursor.getFilePath().startsWith(StorageManager.getInstance().getMediaDir().getAbsolutePath())) { + Log.d(DEBUG_TAG, "Skipping media "+ bucketName +" because it's part of the Seadroid cache"); + }else { + previous = iterateCursor(syncResult, dataManager, cursor); + } + }else { + previous = null; + } + if(isCancelled()){ + Utils.utilsLogInfo(true, "========Cancel uploading========"); + break; } - } finally { - if (cursor != null) - cursor.close(); } - } - private String varArgs(int count) { - String[] chars = new String[count]; - Arrays.fill(chars, "?"); - return "( " + Joiner.on(", ").join(chars) + " )"; - } + private MediaCursor iterateCursor(SyncResult syncResult, DataManager dataManager, MediaCursor cursor) throws SeafException, InterruptedException{ + if(cursor == null || cursor.getCount() == 0){ + Utils.utilsLogInfo(true,"=======Empty Cursor.==="); + return null; + } + String bucketName = cursor.getBucketName(); + int fileIter = cursor.getPosition(); + int fileNum = cursor.getCount(); - /** - * Iterate through the content provider and upload all files - * - * @param syncResult - * @param dataManager - * @param cursor - * @throws SeafException - */ - private void iterateCursor(SyncResult syncResult, DataManager dataManager, Cursor cursor, String media) throws SeafException, InterruptedException { - - tasksInProgress.clear(); - File file; - // upload them one by one - while (!isCancelled() && cursor.moveToNext()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (media.equals("images")) { - String image_id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); - Uri image_uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, image_id); - if (image_uri == null) { - syncResult.stats.numSkippedEntries++; - continue; + Utils.utilsLogInfo(true, "========Found ["+ fileIter+"/"+fileNum+"] images in bucket "+bucketName+"========"); + try { + DirentCache cache = getCache(targetRepoId, bucketName, dataManager); + if(cache != null) { + int n = cache.getCount(); + boolean done = false; + for (int i = 0; i < n; ++i) { + if (cursor.isAfterLast()) { + break; + } + SeafDirent item = cache.get(i); + while (cursor.getFileName().compareTo(item.name) <= 0) { + if (cursor.getFileName().compareTo(item.name) < 0 || item.size < cursor.getFileSize() && item.mtime < cursor.getFileModified()) { + File file = cursor.getFile(); + if (file != null && file.exists() && dbHelper.isUploaded(file.getPath(), file.lastModified())) { + Log.d(DEBUG_TAG, "Skipping media " + file.getPath() + " because we have uploaded it in the past."); + } else { + if (file == null || !file.exists()) { + Log.d(DEBUG_TAG, "Skipping media " + file + " because it doesn't exist"); + syncResult.stats.numSkippedEntries++; + } else { + uploadFile(dataManager, file, bucketName); + } + } + } else { +// ++uploadedNum; +// if(uploadedNum > leaveMeida){ +// if(cursor.deleteFile()){ +// Utils.utilsLogInfo(true, "====File " + cursor.getFileName() + " in bucket " + bucketName + " is deleted because it exists on the server. Skipping."); +// } +// } + Log.d(DEBUG_TAG, "====File " + cursor.getFilePath() + " in bucket " + bucketName + " already exists on the server. Skipping."); + dbHelper.markAsUploaded(cursor.getFilePath(), cursor.getFileModified()); + } + ++fileIter; + if (!cursor.moveToNext()) { + break; + } + } + if (i == n - 1) { + done = true; } - file = new File(Utils.getRealPathFromURI(SeadroidApplication.getAppContext(), image_uri, media)); - } else { - String video_id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)); - Uri video_uri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, video_id); - if (video_uri == null) { - syncResult.stats.numSkippedEntries++; - continue; + if (isCancelled()) { + break; } - file = new File(Utils.getRealPathFromURI(SeadroidApplication.getAppContext(), video_uri, media)); + } - } else { - int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); - if (cursor.getString(dataColumn) == null) { - syncResult.stats.numSkippedEntries++; - continue; + if(done){ + cache.delete(); } - file = new File(cursor.getString(dataColumn)); - - } -// Utils.utilsLogInfo(true,"======iterateCursor"); - int bucketColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME); - String bucketName = cursor.getString(bucketColumn); - - // local file does not exist. some inconsistency in the Media Provider? Ignore and continue - if (!file.exists()) { - // Log.d(DEBUG_TAG, "Skipping media "+file+" because it doesn't exist"); -// Utils.utilsLogInfo(true, "=====Skipping media " + file + " because it doesn't exist"); - syncResult.stats.numSkippedEntries++; - continue; - } - - // Ignore all media by Seafile. We don't want to upload our own cached files. - if (file.getAbsolutePath().startsWith(StorageManager.getInstance().getMediaDir().getAbsolutePath())) { - // Log.d(DEBUG_TAG, "Skipping media "+file+" because it's part of the Seadroid cache"); - Utils.utilsLogInfo(true, "======Skipping media " + file + " because it's part of the Seadroid cache"); - continue; } - - if (dbHelper.isUploaded(file)) { - // Log.d(DEBUG_TAG, "Skipping media " + file + " because we have uploaded it in the past."); - Utils.utilsLogInfo(true, "=====Skipping media " + file + " because we have uploaded it in the past."); - continue; + while (!cursor.isAfterLast() && !isCancelled()) { + File file = cursor.getFile(); + uploadFile(dataManager, file, bucketName); + ++fileIter; + if(!cursor.moveToNext()){ + break; + } } - - uploadFile(dataManager, file, bucketName); + }catch (IOException e){ + Log.e(DEBUG_TAG, "Failed to get cache file.", e); + Utils.utilsLogInfo(true, "Failed to get cache file: "+ e.toString()); + }catch (Exception e){ + Utils.utilsLogInfo(true, e.toString()); } + Utils.utilsLogInfo(true,"=======Have checked ["+Integer.toString(fileIter)+"/"+Integer.toString(fileNum)+"] images in "+bucketName+".==="); Utils.utilsLogInfo(true,"=======waitForUploads==="); waitForUploads(); checkUploadResult(syncResult); + if(cursor.isAfterLast()){ + return null; + } + return cursor; } private void waitForUploads() throws InterruptedException { @@ -592,14 +626,18 @@ private void checkUploadResult(SyncResult syncResult) throws SeafException { for (int id: tasksInProgress) { UploadTaskInfo info = txService.getUploadTaskInfo(id); if (info.err != null) { - throw info.err; +// Utils.utilsLogInfo(true,"=======Task "+ Integer.toString(id) + " fail to uploaded!!!"); +// throw info.err; + continue; } if (info.state == TaskState.FINISHED) { File file = new File(info.localFilePath); dbHelper.markAsUploaded(file); syncResult.stats.numInserts++; } else { - throw SeafException.unknownException; +// Utils.utilsLogInfo(true,"=======Task "+ Integer.toString(id) + " has an unknown exception!!!"); +// throw SeafException.unknownException; + continue; } } } @@ -613,38 +651,8 @@ private void checkUploadResult(SyncResult syncResult) throws SeafException { * @throws SeafException */ private void uploadFile(DataManager dataManager, File file, String bucketName) throws SeafException { - String serverPath = Utils.pathJoin(BASE_DIR, bucketName); - Utils.utilsLogInfo(true,"=======uploadFile==="); - List list = dataManager.getCachedDirents(targetRepoId, serverPath); - if (list == null) { - Log.e(DEBUG_TAG, "Seadroid dirent cache is empty in uploadFile. Should not happen, aborting."); - Utils.utilsLogInfo(true,"=======Seadroid dirent cache is empty in uploadFile. Should not happen, aborting."); - // the dirents were supposed to be refreshed in createDirectories() - // something changed, abort. - throw SeafException.unknownException; - } - - /* - * We don't want to upload a file twice unless the local and remote files differ. - * - * It would be cool if the API2 offered a way to query the hash of a remote file. - * Currently, comparing the file size is the best we can do. - */ - String filename = file.getName(); - String prefix = filename.substring(0, filename.lastIndexOf(".")); - String suffix = filename.substring(filename.lastIndexOf(".")); - Pattern pattern = Pattern.compile(Pattern.quote(prefix) + "( \\(\\d+\\))?" + Pattern.quote(suffix)); - for (SeafDirent dirent : list) { - if (pattern.matcher(dirent.name).matches() && dirent.size == file.length()) { - // Log.d(DEBUG_TAG, "File " + file.getName() + " in bucket " + bucketName + " already exists on the server. Skipping."); - Utils.utilsLogInfo(true,"====File " + file.getName() + " in bucket " + bucketName + " already exists on the server. Skipping."); - dbHelper.markAsUploaded(file); - return; - } - } - - // Log.d(DEBUG_TAG, "uploading file " + file.getName() + " to " + serverPath); + Log.d(DEBUG_TAG, "uploading file " + file.getName() + " to " + serverPath); Utils.utilsLogInfo(true,"====uploading file " + file.getName() + " to " + serverPath); int taskID = txService.addUploadTask(dataManager.getAccount(), targetRepoId, targetRepoName, serverPath, file.getAbsolutePath(), false, false); @@ -713,4 +721,260 @@ private void showNotificationRepoError() { mNotificationManager.notify(0, mBuilder.build()); } + protected class MediaCursor{ + private Cursor imageCursor; + private Cursor videoCursor; + private String bucketName; + private Cursor p; + private Comparator comp; + + protected class DefaultCompartor implements Comparator { + @Override + public int compare(Cursor o1, Cursor o2) { + if(o1 == null || o1.isAfterLast()){ + return 1; + } + if(o2 == null || o2.isAfterLast()){ + return -1; + } + return o1.getString(o1.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)).compareTo(o2.getString(o2.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME))); + } + }; + + public MediaCursor(String bucketName, Cursor imageCursor, Cursor videoCursor){ + init(bucketName, imageCursor, videoCursor, new DefaultCompartor()); + } + + public MediaCursor(String bucketName, Cursor imageCursor, Cursor videoCursor, Comparator comp){ + if(comp == null){ + comp = new DefaultCompartor(); + } + init(bucketName, imageCursor, videoCursor, comp); + } + + private Cursor initializeCursor(Cursor cursor){ + if(cursor != null && cursor.isBeforeFirst()){ + cursor.moveToNext(); + } + if(cursor == null || cursor.isAfterLast()){ + if(cursor != null){ + cursor.close(); + } + return null; + } + return cursor; + } + + private void init(String bucketName, Cursor imageCursor, Cursor videoCursor, Comparator comp){ + this.bucketName = bucketName; + imageCursor = initializeCursor(imageCursor); + videoCursor = initializeCursor(videoCursor); + this.imageCursor = imageCursor; + this.videoCursor = videoCursor; + if(this.imageCursor == null && this.videoCursor == null){ + this.p = null; + }else if(this.imageCursor == null){ + this.p = videoCursor; + }else if(this.videoCursor == null){ + this.p = imageCursor; + }else{ + if(comp.compare(imageCursor, videoCursor) <= 0){ + this.p = imageCursor; + }else{ + this.p = videoCursor; + } + } + this.comp = comp; + } + + + public int getCount(){ + if(imageCursor == null && videoCursor == null){ + return 0; + }else if(imageCursor == null){ + return videoCursor.getCount(); + }else if(videoCursor == null){ + return imageCursor.getCount(); + }else{ + return imageCursor.getCount() + videoCursor.getCount(); + } + } + + public int getPosition(){ + if(imageCursor == null && videoCursor == null){ + return 0; + }else if(imageCursor == null){ + return videoCursor.getPosition(); + }else if(videoCursor == null){ + return imageCursor.getPosition(); + }else{ + return imageCursor.getPosition() + videoCursor.getPosition(); + } + } + + public boolean isAfterLast(){ + return (this.imageCursor == null || this.imageCursor.isAfterLast()) + && (this.videoCursor == null || this.videoCursor.isAfterLast()); + } + + public boolean moveToNext(){ + if(isAfterLast() || p == null){ + return false; + } + boolean res = p.moveToNext(); + if(comp.compare(imageCursor, videoCursor) <= 0){ + p = imageCursor; + }else{ + p = videoCursor; + } + return res; + } + + public String getFileName(){ + if(p == null){ + return ""; + } + if(p == imageCursor){ + return p.getString(p.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)); + }else{ + return p.getString(p.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)); + } + } + + public int getFileModified(){ + if(p == null){ + return -1; + } + if(p == imageCursor){ + return p.getInt(p.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED)); + }else{ + return p.getInt(p.getColumnIndex(MediaStore.Video.Media.DATE_MODIFIED)); + } + } + + public File getFile(){ + String path = getFilePath(); + if(path == null || path.length() == 0){ + return null; + } + return new File(getFilePath()); + } + + public Uri getFileUri(){ + if(p == null){ + return Uri.EMPTY; + } + if(p == imageCursor){ + String id = p.getString(p.getColumnIndex(MediaStore.Images.Media._ID)); + return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); + }else{ + String id = p.getString(p.getColumnIndex(MediaStore.Video.Media._ID)); + return Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); + } + } + + public String getFileID(){ + if(p == null){ + return ""; + } + if(p == imageCursor){ + return p.getString(p.getColumnIndex(MediaStore.Images.Media._ID)); + }else{ + return p.getString(p.getColumnIndex(MediaStore.Video.Media._ID)); + } + } + + public String getFilePath(){ + if(p == null){ + return ""; + } + if(p == imageCursor){ + String id = p.getString(p.getColumnIndex(MediaStore.Images.Media._ID)); + Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); + if(uri == null){ + return null; + } + return Utils.getRealPathFromURI(SeadroidApplication.getAppContext(), uri, "images"); + }else{ + String id = p.getString(p.getColumnIndex(MediaStore.Video.Media._ID)); + Uri uri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); + if(uri == null){ + return null; + } + return Utils.getRealPathFromURI(SeadroidApplication.getAppContext(), uri, "video"); + } + } + + public int getFileSize(){ + if(p == null){ + return -1; + } + if(p == imageCursor){ + return p.getInt(p.getColumnIndex(MediaStore.Images.Media.SIZE)); + }else{ + return p.getInt(p.getColumnIndex(MediaStore.Video.Media.SIZE)); + } + } + + public String getBucketName(){ + if(p == null){ + return ""; + } + if(p.isBeforeFirst()){ + return bucketName; + } + if(p == imageCursor){ + return p.getString(p.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)); + }else{ + return p.getString(p.getColumnIndex(MediaStore.Video.Media.BUCKET_DISPLAY_NAME)); + } + } + + public boolean deleteFile(){ + File file = getFile(); + if(file == null || !file.exists()){ + return false; + } + return file.delete(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if(imageCursor!=null) { + imageCursor.close(); + } + if(videoCursor!=null) { + videoCursor.close(); + } + } + } + + private DirentCache getCache(String repoID, String bucketName, DataManager dataManager) throws IOException, InterruptedException, SeafException{ + String name = CACHE_NAME+repoID+"-"+bucketName; + String serverPath = Utils.pathJoin(BASE_DIR, bucketName); +// if(DirentCache.cacheFileExists(name)){ +// return new DirentCache(name); +// } + Utils.utilsLogInfo(true, "=======savingRepo==="); + List seafDirents = dataManager.getCachedDirents(repoID, serverPath); + int timeOut = 10000; // wait up to a second + while (seafDirents == null && timeOut > 0) { + // Log.d(DEBUG_TAG, "waiting for transfer service"); + Thread.sleep(100); + seafDirents = dataManager.getDirentsFromServer(targetRepoId, serverPath); + timeOut -= 100; + } + if (seafDirents == null) { + return null; + } + return new DirentCache(name, seafDirents, new Comparator() { + @Override + public int compare(SeafDirent o1, SeafDirent o2) { + return o1.name.compareTo(o2.name); + } + }); + } } + + diff --git a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncService.java b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncService.java index 797d8ff59..9059f2e97 100644 --- a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncService.java +++ b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraSyncService.java @@ -1,9 +1,14 @@ package com.seafile.seadroid2.cameraupload; import android.app.Service; +import android.content.ContentResolver; import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.AccountManager; + /** * Camera Sync Service. * @@ -14,6 +19,11 @@ public class CameraSyncService extends Service { private static CameraSyncAdapter sSyncAdapter = null; private static final Object sSyncAdapterLock = new Object(); + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + @Override public void onCreate() { synchronized (sSyncAdapterLock) { diff --git a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraUploadDBHelper.java b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraUploadDBHelper.java index 3b573942f..50bceeb2e 100644 --- a/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraUploadDBHelper.java +++ b/app/src/main/java/com/seafile/seadroid2/cameraupload/CameraUploadDBHelper.java @@ -7,8 +7,12 @@ import android.database.sqlite.SQLiteOpenHelper; import com.seafile.seadroid2.SeadroidApplication; +import com.seafile.seadroid2.data.SeafDirent; +import com.seafile.seadroid2.util.Utils; import java.io.File; +import java.util.List; +import java.util.regex.Pattern; public class CameraUploadDBHelper extends SQLiteOpenHelper { private static final String DEBUG_TAG = "CameraUploadDBHelper"; @@ -30,7 +34,7 @@ public class CameraUploadDBHelper extends SQLiteOpenHelper { "CREATE TABLE " + PHOTOCACHE_TABLE_NAME + " (" + PHOTOCACHE_COLUMN_ID + " INTEGER PRIMARY KEY, " + PHOTOCACHE_COLUMN_FILE + " TEXT NOT NULL, " - + PHOTOCACHE_COLUMN_DATE_ADDED + " BIGINT NOT NULL);"; + + PHOTOCACHE_COLUMN_DATE_ADDED + " BIGINT NOT NULL)"; private static final String[] projection = { PHOTOCACHE_COLUMN_ID, @@ -58,12 +62,13 @@ public void onCreate(SQLiteDatabase db) { private void createPhotoCacheTable(SQLiteDatabase db) { db.execSQL(SQL_CREATE_PHOTOCACHE_TABLE); - db.execSQL("CREATE INDEX photo_repoid_index ON " + PHOTOCACHE_TABLE_NAME + db.execSQL("CREATE INDEX photo_file_index ON " + PHOTOCACHE_TABLE_NAME + " (" + PHOTOCACHE_COLUMN_FILE + ");"); - db.execSQL("CREATE INDEX photo_account_index ON " + PHOTOCACHE_TABLE_NAME + db.execSQL("CREATE INDEX photo_date_index ON " + PHOTOCACHE_TABLE_NAME + " (" + PHOTOCACHE_COLUMN_DATE_ADDED + ");"); } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + PHOTOCACHE_TABLE_NAME + ";"); @@ -75,10 +80,8 @@ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } - public boolean isUploaded(File file) { - String path = file.getAbsolutePath(); - long modified = file.lastModified(); + public boolean isUploaded(String path, long modified) { Cursor c = database.query( PHOTOCACHE_TABLE_NAME, projection, @@ -98,6 +101,10 @@ public void markAsUploaded(File file) { String path = file.getAbsolutePath(); long modified = file.lastModified(); + markAsUploaded(path, modified); + } + + public void markAsUploaded(String path, long modified) { ContentValues values = new ContentValues(); values.put(PHOTOCACHE_COLUMN_FILE, path); values.put(PHOTOCACHE_COLUMN_DATE_ADDED, modified); diff --git a/app/src/main/java/com/seafile/seadroid2/data/CacheItem.java b/app/src/main/java/com/seafile/seadroid2/data/CacheItem.java new file mode 100644 index 000000000..9c634eaab --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/CacheItem.java @@ -0,0 +1,233 @@ +package com.seafile.seadroid2.data; + +import com.google.common.collect.Lists; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.util.Utils; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +public abstract class CacheItem { + + private static final String FILE_SUFFIX = ".dat"; + private static final StorageManager storageManager = StorageManager.getInstance(); + private static final byte[] split = ",".getBytes(StandardCharsets.UTF_8); + private static final byte[] left = "[".getBytes(StandardCharsets.UTF_8); + private static final byte[] right = "]".getBytes(StandardCharsets.UTF_8); + + private String name; + private List positions; + private List itemSizes; + private RandomAccessFile file; + + public static String getConfigFilePath(String name){ + return Utils.pathJoin(storageManager.getJsonCacheDir().getAbsolutePath(), name+"-config"+FILE_SUFFIX); + } + + public static String getDataFilePath(String name){ + return Utils.pathJoin(storageManager.getJsonCacheDir().getAbsolutePath(), name+FILE_SUFFIX); + } + + public static boolean cacheFileExists(String name){ + File configFile = new File(getConfigFilePath(name)); + if(!configFile.exists()){ + return false; + } + File dataFile = new File(getDataFilePath(name)); + if(!dataFile.exists()){ + return false; + } + return true; + } + + public CacheItem(String name) throws IOException{ + this.name = name; + readContents(name); + } + + public CacheItem(String name, List caches, Comparator comp) throws IOException{ + this.name = name; + saveContents(caches, comp); + } + + protected abstract byte[] toBytes(T item); + + protected abstract T fromBytes(byte[] bytes); + + public void saveContents(List caches, Comparator comp) throws IOException { + if(caches == null){ + return; + } + caches.sort(comp); + FileOutputStream os = null; + FileOutputStream configOs = null; + positions = Lists.newArrayList(); + itemSizes = Lists.newArrayList(); + try { + os = new FileOutputStream(getDataFilePath(name)); + configOs = new FileOutputStream(getConfigFilePath(name)); + os.write(left); + int position = left.length; + for(T item: caches){ + if(position != left.length){ + os.write(split); + position += split.length; + } + byte[] content = toBytes(item); + os.write(content); + configOs.write(intToBytesLittleEndian(content.length)); + itemSizes.add(content.length); + positions.add(position); + position += content.length; + } + os.write(right); + os.close(); + configOs.close(); + file = new RandomAccessFile(new File(getDataFilePath(name)), "r"); + } finally { + try { + if (os != null) { + os.close(); + } + if(configOs != null){ + configOs.close(); + } + } catch (Exception e) { + // ignore + } + } + } + + public void readContents(String name) throws IOException{ + FileInputStream is = null; + try { + is = new FileInputStream(getConfigFilePath(name)); + byte[] buffer = new byte[Integer.BYTES]; + positions = Lists.newArrayList(); + itemSizes = Lists.newArrayList(); + int position = left.length; + while (true) { + if(position != left.length){ + position += split.length; + } + int len = is.read(buffer); + if (len == -1) + break; + int size = bytesToIntLittleEndian(buffer); + itemSizes.add(size); + positions.add(position); + position += size; + } + is.close(); + file = new RandomAccessFile(new File(getDataFilePath(name)), "r"); + }finally { + try { + if (is != null) { + is.close(); + } + } catch (Exception e) { + // ignore + } + } + } + + public int getCount(){ + if(positions == null){ + return 0; + } + return positions.size(); + } + + public T get(int i){ + if(i >= getCount() || i < 0){ + return null; + } + try { + file.seek(positions.get(i)); + byte[] buffer = new byte[itemSizes.get(i)]; + int len = file.read(buffer); + if(len != itemSizes.get(i)){ + return null; + } + return fromBytes(buffer); + }catch (IOException e){ + return null; + } + } + + public List getAll() throws IOException{ + List res = Lists.newArrayList(); + int n = getCount(); + for(int i=0;i> Byte.SIZE) & 0xff); + bytes[2] = (byte) ((value >> Byte.SIZE * 2) & 0xff); + bytes[3] = (byte) ((value >> Byte.SIZE * 3) & 0xff); + return bytes; + } + + public static byte[] intToBytesBigEndian(int value) { + byte[] bytes = new byte[Integer.BYTES]; + bytes[3] = (byte) (value & 0xff); + bytes[2] = (byte) ((value >> Byte.SIZE) & 0xff); + bytes[1] = (byte) ((value >> Byte.SIZE * 2) & 0xff); + bytes[0] = (byte) ((value >> Byte.SIZE * 3) & 0xff); + return bytes; + } + + +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/DataManager.java b/app/src/main/java/com/seafile/seadroid2/data/DataManager.java index 74fa0036a..700cfd7a1 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/DataManager.java +++ b/app/src/main/java/com/seafile/seadroid2/data/DataManager.java @@ -482,7 +482,7 @@ public synchronized File getFile(String repoName, String repoID, String path, } public synchronized File getFileByBlocks(String repoName, String repoID, String path, long fileSize, - ProgressMonitor monitor) throws SeafException, IOException, JSONException, NoSuchAlgorithmException { + ProgressMonitor monitor) throws SeafException, IOException, JSONException, NoSuchAlgorithmException { String cachedFileID = null; SeafCachedFile cf = getCachedFile(repoName, repoID, path); @@ -638,6 +638,10 @@ public List getDirentsFromServer(String repoID, String path) throws return parseDirents(content); } + public String getDirID(String repoID, String path){ + return dbHelper.getCachedDirents(repoID, path); + } + public List getStarredFiles() throws SeafException { String starredFiles = sc.getStarredFiles(); Log.v(DEBUG_TAG, "Save starred files: " + starredFiles); diff --git a/app/src/main/java/com/seafile/seadroid2/data/DirentCache.java b/app/src/main/java/com/seafile/seadroid2/data/DirentCache.java new file mode 100644 index 000000000..3b286d92b --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/DirentCache.java @@ -0,0 +1,42 @@ +package com.seafile.seadroid2.data; + +import com.seafile.seadroid2.SeadroidApplication; +import com.seafile.seadroid2.util.PinyinUtils; + +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; + +public class DirentCache extends CacheItem{ + + public DirentCache(String name) throws IOException { + super(name); + } + + public DirentCache(String name, List caches) throws IOException{ + super(name, caches, new SeafDirent.DirentNameComparator()); + } + + public DirentCache(String name, List caches, Comparator comp) throws IOException{ + super(name, caches, comp); + } + + protected byte[] toBytes(SeafDirent item){ + JSONObject json = item.toJson(); + String content = json.toString(); + return content.getBytes(StandardCharsets.UTF_8); + } + + protected SeafDirent fromBytes(byte[] bytes){ + String content = new String(bytes, StandardCharsets.UTF_8); + try { + JSONObject json = new JSONObject(content); + return SeafDirent.fromJson(json); + }catch (Exception e){ + return null; + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/SeafCachedFile.java b/app/src/main/java/com/seafile/seadroid2/data/SeafCachedFile.java index 209954fd8..a9bce838a 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/SeafCachedFile.java +++ b/app/src/main/java/com/seafile/seadroid2/data/SeafCachedFile.java @@ -56,4 +56,12 @@ public long getFileOriginalSize() { public void setFileOriginalSize(long fileOriginalSize) { this.fileOriginalSize = fileOriginalSize; } + + public String getFileName(){ + return path.substring(path.lastIndexOf('/') + 1); + } + + public String getDirentPath(){ + return path.substring(0, path.lastIndexOf('/') + 1); + } } diff --git a/app/src/main/java/com/seafile/seadroid2/data/SeafDirent.java b/app/src/main/java/com/seafile/seadroid2/data/SeafDirent.java index 2592dfdac..32bac3d6b 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/SeafDirent.java +++ b/app/src/main/java/com/seafile/seadroid2/data/SeafDirent.java @@ -24,7 +24,7 @@ public enum DirentType {DIR, FILE} public long size; // size of file, 0 if type is dir public long mtime; // last modified timestamp - static SeafDirent fromJson(JSONObject obj) { + public static SeafDirent fromJson(JSONObject obj) { SeafDirent dirent = new SeafDirent(); try { dirent.id = obj.getString("id"); @@ -44,6 +44,26 @@ static SeafDirent fromJson(JSONObject obj) { } } + public JSONObject toJson(){ + try { + JSONObject json = new JSONObject(); + json.put("id", this.id); + json.put("name", this.name); + json.put("mtime", this.mtime); + json.put("permission", this.permission); + if(this.type == DirentType.FILE){ + json.put("type", "file"); + }else{ + json.put("type", "dir"); + } + json.put("size", this.size); + return json; + } catch (JSONException e) { + Log.d(DEBUG_TAG, e.getMessage()); + return null; + } + } + public boolean isDir() { return (type == DirentType.DIR); } diff --git a/app/src/main/java/com/seafile/seadroid2/data/SeafRepo.java b/app/src/main/java/com/seafile/seadroid2/data/SeafRepo.java index 26a147bee..0d93bcf32 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/SeafRepo.java +++ b/app/src/main/java/com/seafile/seadroid2/data/SeafRepo.java @@ -31,7 +31,7 @@ public class SeafRepo implements SeafItem { public long size; public String root; // the id of root directory - static SeafRepo fromJson(JSONObject obj) throws JSONException{ + public static SeafRepo fromJson(JSONObject obj) throws JSONException{ SeafRepo repo = new SeafRepo(); repo.id = obj.getString("id"); repo.name = obj.getString("name"); @@ -49,6 +49,30 @@ static SeafRepo fromJson(JSONObject obj) throws JSONException{ return repo; } + public JSONObject toJson() throws JSONException{ + JSONObject json = new JSONObject(); + json.put("id", this.id); + json.put("name", this.name); + json.put("owner", this.owner); + json.put("permission", this.permission); + json.put("mtime", this.mtime); + json.put("encrypted", this.encrypted); + json.put("root", this.root); + json.put("size", this.size); + if(this.isGroupRepo){ + json.put("type", "grepo"); + }else if(this.isPersonalRepo){ + json.put("type", "repo"); + }else if(this.isSharedRepo){ + json.put("type", "srepo"); + }else{ + json.put("type", ""); + } + json.put("magic", this.magic); + json.put("random_key", this.encKey); + return json; + } + public SeafRepo() { } diff --git a/app/src/main/java/com/seafile/seadroid2/data/StorageManager.java b/app/src/main/java/com/seafile/seadroid2/data/StorageManager.java index 5666f4c6d..eb5895ba9 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/StorageManager.java +++ b/app/src/main/java/com/seafile/seadroid2/data/StorageManager.java @@ -14,6 +14,7 @@ import com.seafile.seadroid2.SettingsManager; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.AccountManager; +import com.seafile.seadroid2.util.Utils; import org.apache.commons.io.FileUtils; @@ -94,12 +95,15 @@ public final static StorageManager getInstance() { private Location buildClassicLocation() { Location classic = new Location(); classic.id = -1; // Android IDs start at 0. so "-1" is safe for us - File[] externalMediaDirs = SeadroidApplication.getAppContext().getExternalMediaDirs(); - String rootPath = externalMediaDirs[0].getAbsolutePath(); + File[] externalMediaDirs = SeadroidApplication.getAppContext().getExternalFilesDirs(Environment.DIRECTORY_DOWNLOADS); + String mediaPath = externalMediaDirs[0].getAbsolutePath(); + + externalMediaDirs = SeadroidApplication.getAppContext().getExternalMediaDirs(); + String cachePath = Utils.pathJoin(externalMediaDirs[0].getAbsolutePath(), "/Seafile/"); // String rootPath = SeadroidApplication.getAppContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(); // classic.mediaPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Seafile/"); - classic.mediaPath = new File(rootPath + "/Seafile/"); - classic.cachePath = new File(classic.mediaPath, "cache"); + classic.mediaPath = new File(mediaPath + "/Seafile/"); + classic.cachePath = new File(cachePath, "cache"); fillLocationInfo(classic); return classic; } diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryAdapter.java b/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryAdapter.java new file mode 100644 index 000000000..efb2d5aab --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryAdapter.java @@ -0,0 +1,158 @@ +package com.seafile.seadroid2.loopimages; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.common.collect.Lists; +import com.mcxtzhang.swipemenulib.SwipeMenuLayout; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeadroidApplication; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.data.DataManager; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Cloud library adapter + */ +public class ChosenLibraryAdapter extends BaseAdapter { + + private List dirInfos; + + private Map accountDataManager; + + public ChosenLibraryAdapter() { + dirInfos = Lists.newLinkedList(); + accountDataManager = new HashMap(); + } + + /** sort files type */ + public static final int SORT_BY_NAME = 9; + /** sort files type */ + public static final int SORT_BY_LAST_MODIFIED_TIME = 10; + /** sort files order */ + public static final int SORT_ORDER_ASCENDING = 11; + /** sort files order */ + public static final int SORT_ORDER_DESCENDING = 12; + + private DataManager getDataManager(Account account){ + if (!accountDataManager.containsKey(account.getSignature())) { + accountDataManager.put(account.getSignature(), new DataManager(account)); + } + return accountDataManager.get(account.getSignature()); + } + + @Override + public int getCount() { + return dirInfos.size(); + } + + @Override + public boolean isEmpty() { + return dirInfos.isEmpty(); + } + + @Override + public DirInfo getItem(int position) { + return dirInfos.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + public void clearDirs() { + dirInfos.clear(); + } + + public void setDirs(List dirs) { + clearDirs(); + for (DirInfo dir : dirs) { + this.dirInfos.add(dir); + } + notifyDataSetChanged(); + } + + public void addDir(DirInfo dir){ + this.dirInfos.add(dir); + notifyDataSetChanged(); + } + + public void sortDirs(int order) { + Collections.sort(dirInfos, new DirInfo.DirInfoComparator()); + if (order == SORT_ORDER_DESCENDING) { + Collections.reverse(dirInfos); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SwipeMenuLayout view = (SwipeMenuLayout) convertView; + ChosenLibraryAdapter.Viewholder viewHolder; + DirInfo dir = dirInfos.get(position); + + if (convertView == null) { + view = (SwipeMenuLayout) LayoutInflater.from(SeadroidApplication.getAppContext()). + inflate(R.layout.loop_images_chosen_library_list_item, null); + TextView title = (TextView) view.findViewById(R.id.loopimages_list_item_title); + TextView subtitle = (TextView) view.findViewById(R.id.loopimages_list_item_subtitle); + ImageView icon = (ImageView) view.findViewById(R.id.loopimages_list_item_icon); + Button action = (Button) view.findViewById(R.id.loopimages_list_item_action); + viewHolder = new ChosenLibraryAdapter.Viewholder(title, subtitle, icon, action); + view.setTag(viewHolder); + } else { + view.quickClose(); + viewHolder = (ChosenLibraryAdapter.Viewholder) convertView.getTag(); + } + + viewHolder.icon.setImageResource(getDataManager(dir.getAccount()).getCachedRepoByID(dir.getRepoId()).getIcon()); + viewHolder.title.setText(dir.getRepoName() + ":" + dir.getDirPath()); + viewHolder.subtitle.setText(dir.getAccount().getDisplayName()); + viewHolder.action.setTag(position); + viewHolder.action.setOnClickListener(new DeleteItemOnClickListener()); + + viewHolder.title.setTextColor(Color.BLACK); + viewHolder.subtitle.setTextColor(Color.GRAY); + if (android.os.Build.VERSION.SDK_INT >= 11) { + viewHolder.icon.setAlpha(255); + } + return view; + } + + private class DeleteItemOnClickListener implements Button.OnClickListener{ + @Override + public void onClick(View v) { + Integer position = (Integer) v.getTag(); + deleteItem(position); + } + } + + private void deleteItem(int position){ + dirInfos.remove(position); + notifyDataSetChanged(); + } + + private class Viewholder { + TextView title, subtitle; + ImageView icon; + Button action; + + public Viewholder(TextView title, TextView subtitle, ImageView icon, Button action) { + super(); + this.icon = icon; + this.title = title; + this.subtitle = subtitle; + this.action = action; + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryFragment.java b/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryFragment.java new file mode 100644 index 000000000..c40ab92ee --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/ChosenLibraryFragment.java @@ -0,0 +1,115 @@ +package com.seafile.seadroid2.loopimages; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ListView; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.ui.activity.SeafilePathChooserActivity; + +import java.util.ArrayList; +import java.util.List; + +/** + * A simple {@link Fragment} subclass. + */ +public class ChosenLibraryFragment extends Fragment { + private Button mDoneBtn, mAddBtn; + + private ListView chosenlibraryListView; + private ChosenLibraryAdapter chosenlibraryListAdapter; + private LoopImagesWidgetConfigureActivity mActivity; + + public ChosenLibraryFragment() { + // Required empty public constructor + } + + public void saveDirInfo(){ + List dirInfoStrs = new ArrayList(); + for(int i=0;i dirInfos = mActivity.getDirInfo(context, mActivity.getAppWidgetId()); + + chosenlibraryListAdapter.setDirs(dirInfos); + + if (chosenlibraryListAdapter.getCount() > 0) + mDoneBtn.setVisibility(View.VISIBLE); + + return rootView; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onActivityResult (int requestCode, int resultCode, Intent data){ + if(resultCode != mActivity.RESULT_OK){ + return; + } + final String repoName = data.getStringExtra(SeafilePathChooserActivity.DATA_REPO_NAME); + final String repoId = data.getStringExtra(SeafilePathChooserActivity.DATA_REPO_ID); + final String dirPath = data.getStringExtra(SeafilePathChooserActivity.DATA_DIRECTORY_PATH); + final String dirId = data.getStringExtra(SeafilePathChooserActivity.DATA_DIR); + final Account account = data.getParcelableExtra(SeafilePathChooserActivity.DATA_ACCOUNT); + + if (repoName != null && repoId != null && dirPath != null && dirId != null && account != null) { + chosenlibraryListAdapter.addDir(new DirInfo(account, repoId, repoName, dirId, dirPath)); + if (chosenlibraryListAdapter.getCount() > 0) + mDoneBtn.setVisibility(View.VISIBLE); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/DirInfo.java b/app/src/main/java/com/seafile/seadroid2/loopimages/DirInfo.java new file mode 100644 index 000000000..e89e6c6cd --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/DirInfo.java @@ -0,0 +1,157 @@ +package com.seafile.seadroid2.loopimages; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeadroidApplication; +import com.seafile.seadroid2.SettingsManager; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.AccountManager; +import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.data.SeafDirent; +import com.seafile.seadroid2.ui.NavContext; +import com.seafile.seadroid2.util.PinyinUtils; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DirInfo { + private Account account; + private String repoId; + private String repoName; + private String dirId; + private String dirPath; + + public static final String LOOPIMAGES_ACCOUNT_SIGNATURE_KEY = "loopimages_account_signature"; + public static final String LOOPIMAGES_REPO_ID_KEY = "loopimages_repo_id"; + public static final String LOOPIMAGES_REPO_NAME_KEY = "loopimages_repo_name"; + public static final String LOOPIMAGES_DIR_ID_KEY = "loopimages_dir_id"; + public static final String LOOPIMAGES_DIR_PATH_KEY = "loopimages_dir_path"; + + public static final String spliter = ","; + + public DirInfo(Account account, NavContext nav){ + this.account = account; + this.repoId = nav.getRepoID(); + this.repoName = nav.getRepoName(); + this.dirId = nav.getDirID(); + this.dirPath = nav.getDirPath(); + } + + public DirInfo(Account account, Map info){ + if(!info.containsKey(LOOPIMAGES_REPO_ID_KEY) || + !info.containsKey(LOOPIMAGES_REPO_NAME_KEY) || + !info.containsKey(LOOPIMAGES_DIR_ID_KEY) || + !info.containsKey(LOOPIMAGES_DIR_PATH_KEY)){ + this.account = null; + this.repoId = null; + this.repoName = null; + this.dirId = null; + this.dirPath = null; + }else{ + this.account = account; + this.repoId = info.get(LOOPIMAGES_REPO_ID_KEY); + this.repoName = info.get(LOOPIMAGES_REPO_NAME_KEY); + this.dirId = info.get(LOOPIMAGES_DIR_ID_KEY); + this.dirPath = info.get(LOOPIMAGES_DIR_PATH_KEY); + } + } + + public DirInfo(Account account, String repoId, String repoName, String dirId, String dirPath){ + this.account = account; + this.repoId = repoId; + this.repoName = repoName; + this.dirId = dirId; + this.dirPath = dirPath; + } + + public Account getAccount(){ + return this.account; + } + + public String getRepoId(){ + return this.repoId; + } + + public String getRepoName(){ + return this.repoName; + } + + public String getDirId(){ + return this.dirId; + } + + public void setDirId(String dirId){ + this.dirId = dirId; + } + + public String getDirPath(){ + return this.dirPath; + } + + public List getSeafDirents(){ + DataManager dataManager = new DataManager(this.account); + return dataManager.getCachedDirents(this.repoId, this.dirPath); + } + + public Map toMap(){ + Map res = new HashMap(); + res.put(LOOPIMAGES_ACCOUNT_SIGNATURE_KEY, getAccount().getSignature()); + res.put(LOOPIMAGES_REPO_ID_KEY, getRepoId()); + res.put(LOOPIMAGES_REPO_NAME_KEY, getRepoName()); + res.put(LOOPIMAGES_DIR_ID_KEY, getDirId()); + res.put(LOOPIMAGES_DIR_PATH_KEY, getDirPath()); + return res; + } + + public String toString(){ + String s = ""; + s += getAccount().getSignature(); + s += spliter + getRepoId(); + s += spliter + getRepoName(); + s += spliter + getDirId(); + s += spliter + getDirPath(); + return s; + } + + public static class DirInfoComparator implements Comparator { + + @Override + public int compare(DirInfo itemA, DirInfo itemB) { + if(itemA.getAccount() == itemB.getAccount()){ + if(itemA.getRepoName() == itemB.getRepoName()){ + return compareString(itemA.getDirPath(), itemB.getDirPath()); + } + return compareString(itemA.getRepoName(), itemB.getRepoName()); + } + return compareString(itemA.getAccount().name, itemB.getAccount().name); + } + + private int compareString(String itemA, String itemB){ + // get the first character unicode from each name + int unicodeA = itemA.codePointAt(0); + int unicodeB = itemB.codePointAt(0); + + String strA, strB; + + // both are Chinese words + if ((19968 < unicodeA && unicodeA < 40869) && (19968 < unicodeB && unicodeB < 40869)) { + strA = PinyinUtils.toPinyin(SeadroidApplication.getAppContext(), itemA).toLowerCase(); + strB = PinyinUtils.toPinyin(SeadroidApplication.getAppContext(), itemB).toLowerCase(); + } else if ((19968 < unicodeA && unicodeA < 40869) && !(19968 < unicodeB && unicodeB < 40869)) { + // itemA is Chinese and itemB is English + return 1; + } else if (!(19968 < unicodeA && unicodeA < 40869) && (19968 < unicodeB && unicodeB < 40869)) { + // itemA is English and itemB is Chinese + return -1; + } else { + // both are English words + strA = itemA.toLowerCase(); + strB = itemB.toLowerCase(); + } + return strA.compareTo(strB); + } + } +} + diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/HowToDownloadFragment.java b/app/src/main/java/com/seafile/seadroid2/loopimages/HowToDownloadFragment.java new file mode 100644 index 000000000..197235d81 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/HowToDownloadFragment.java @@ -0,0 +1,61 @@ +package com.seafile.seadroid2.loopimages; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SettingsManager; + +/** + * How to upload fragment + */ +public class HowToDownloadFragment extends Fragment { + + private RadioButton mDataPlanRadioBtn; + private RadioGroup mRadioGroup; + + private LoopImagesWidgetConfigureActivity mActivity; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mActivity = (LoopImagesWidgetConfigureActivity) getActivity(); + + View rootView = mActivity.getLayoutInflater().inflate(R.layout.loop_images_how_to_download_fragment, null); + + mRadioGroup = (RadioGroup) rootView.findViewById(R.id.loopimages_wifi_radio_group); + mDataPlanRadioBtn = (RadioButton) rootView.findViewById(R.id.loopimages_wifi_or_data_plan_rb); + + if (mActivity.getDataPlanAllowed(mActivity.getAppWidgetId())) { + mDataPlanRadioBtn.setChecked(true); + } + + mRadioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case R.id.loopimages_wifi_only_rb: + // WiFi only + mActivity.saveDataPlanAllowed(false); + break; + case R.id.loopimages_wifi_or_data_plan_rb: + // WiFi and data plan + mActivity.saveDataPlanAllowed(true); + break; + } + + } + + }); + + return rootView; + } + +} + diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidget.java b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidget.java new file mode 100644 index 000000000..6a62f01f2 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidget.java @@ -0,0 +1,91 @@ +package com.seafile.seadroid2.loopimages; + +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.view.LayoutInflater; +import android.widget.RelativeLayout; +import android.widget.RemoteViews; + +import com.seafile.seadroid2.R; + +import java.util.List; +import java.util.Map; +import java.util.zip.Inflater; + +/** + * Implementation of App Widget functionality. + * App Widget Configuration implemented in {@link LoopImagesWidgetConfigureActivity LoopImagesWidgetConfigureActivity} + */ +public class LoopImagesWidget extends AppWidgetProvider { + public static final String WIDGET_ID_KEY = "widget_id"; + public static final String DIR_INFO = "dir_info"; + public static final String IMAGE_NAME = "image_name"; + + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + +// CharSequence widgetText = LoopImagesWidgetConfigureActivity.loadTitlePref(context, appWidgetId); +// // Construct the RemoteViews object +// List dirInfos = LoopImagesWidgetConfigureActivity.getDirInfo(appWidgetId); +// +// RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.loop_images_widget); +// views.setImageViewResource(R.id.loopimages_imageview, R.drawable.rem); +//// +//// // Instruct the widget manager to update the widget +// appWidgetManager.updateAppWidget(appWidgetId, views); + Intent intent = new Intent(context, LoopImagesWidgetService.class); + intent.putExtra(LoopImagesWidgetService.UPDATE_IMAGE_INFO_SIGNAL, appWidgetId); + context.startService(intent); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// context.startForegroundService(intent); +// }else{ +// context.startService(intent); +// } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them +// for (int appWidgetId : appWidgetIds) { +// updateAppWidget(context, appWidgetManager, appWidgetId); +// } + Intent intent = new Intent(context, LoopImagesWidgetService.class); + intent.putExtra(LoopImagesWidgetService.UPDATE_IMAGE_INFO_SIGNAL, LoopImagesWidgetService.UPDATE_ALL_WIDGETS); + context.startService(intent); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// context.startForegroundService(intent); +// }else{ +// context.startService(intent); +// } + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // When the user deletes the widget, delete the preference associated with it. + for (int appWidgetId : appWidgetIds) { + LoopImagesWidgetConfigureActivity.deleteDirInfo(appWidgetId); + Intent intent = new Intent(context, LoopImagesWidgetService.class); + intent.putExtra(LoopImagesWidgetService.DELETE_IMAGE_INFO_SIGNAL, appWidgetId); + context.startService(intent); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// context.startForegroundService(intent); +// }else{ +// context.startService(intent); +// } + } + } + + @Override + public void onEnabled(Context context) { + // Enter relevant functionality for when the first widget is created + } + + @Override + public void onDisabled(Context context) { + // Enter relevant functionality for when the last widget is disabled + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetConfigureActivity.java b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetConfigureActivity.java new file mode 100644 index 000000000..05e7ca6cf --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetConfigureActivity.java @@ -0,0 +1,318 @@ +package com.seafile.seadroid2.loopimages; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.TextUtils; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SettingsManager; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.AccountManager; +import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.ui.activity.BaseActivity; +import com.viewpagerindicator.LinePageIndicator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The configuration screen for the {@link LoopImagesWidget LoopImagesWidget} AppWidget. + */ +public class LoopImagesWidgetConfigureActivity extends BaseActivity { + + private static final String PREFS_NAME = "com.seafile.seadroid2.loopimages.LoopImagesWidget"; + private static final String PREF_PREFIX_KEY = "appwidget_"; + public static final int CHOOSE_LOOPIMAGES_REQUEST = 1; + + public static SettingsManager settingsMgr = null; + private static AccountManager accountManager = null; + private static List accountList = null; + private static Map dataManagerMap = null; + private static Map signatureAccount = null; + + private ViewPager mViewPager; + private LinePageIndicator mIndicator; + + private boolean hasImage = false; + private int mCurrentPosition; + + private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + private static LoopImagesWidgetConfigureActivity existActivity = null; + +// View.OnClickListener mOnClickListener = new View.OnClickListener() { +// public void onClick(View v) { +// final Context context = LoopImagesWidgetConfigureActivity.this; +// +// // When the button is clicked, store the string locally +// String widgetText = mAppWidgetText.getText().toString(); +// saveTitlePref(context, mAppWidgetId, widgetText); +// +// // It is the responsibility of the configuration activity to update the app widget +// AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); +// LoopImagesWidget.updateAppWidget(context, appWidgetManager, mAppWidgetId); +// +// // Make sure we pass back the original appWidgetId +// Intent resultValue = new Intent(); +// resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); +// setResult(RESULT_OK, resultValue); +// finish(); +// } +// }; + + public LoopImagesWidgetConfigureActivity() { + super(); + } + + public int getAppWidgetId(){ + return mAppWidgetId; + } + +// // Write the prefix to the SharedPreferences object for this widget +// static void saveTitlePref(Context context, int appWidgetId, String text) { +// SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit(); +// prefs.putString(PREF_PREFIX_KEY + appWidgetId, text); +// prefs.apply(); +// } +// +// // Read the prefix from the SharedPreferences object for this widget. +// // If there is no preference saved, get the default from a resource +// static String loadTitlePref(Context context, int appWidgetId) { +// SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); +// String titleValue = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null); +// if (titleValue != null) { +// return titleValue; +// } else { +// return context.getString(R.string.appwidget_text); +// } +// } + + static public SettingsManager getSettingsManager(){ + if(settingsMgr == null){ + settingsMgr = SettingsManager.instance(); + } + return settingsMgr; + } + + public void saveDataPlanAllowed(boolean isAllowed) { + getSettingsManager().saveLoopImagesWidgetDataPlanAllowed(mAppWidgetId, isAllowed); + } + + static public boolean getDataPlanAllowed(int mAppWidgetId) { + return getSettingsManager().getLoopImagesWidgetDataPlanAllowed(mAppWidgetId); + } + + static public DirInfo getDirInfoFromStringList(Context context, List dirInfo){ + init(context); + if(dirInfo.size() != 5){ + return null; + } + Account account = signatureAccount.get(dirInfo.get(0)); + return new DirInfo(account, dirInfo.get(1), dirInfo.get(2), dirInfo.get(3), dirInfo.get(4)); + } + + static public DirInfo getDirInfoFromString(Context context, String dirInfoStr){ + if(dirInfoStr == null || dirInfoStr.length() == 0){ + return null; + } + init(context); + List infoList = Arrays.asList(TextUtils.split(dirInfoStr, DirInfo.spliter)); + if(infoList.size() != 5){ + return null; + } + return getDirInfoFromStringList(context, infoList); + } + + static public List getDirInfo(Context context, int appWidgetId) { + init(context); + List dirInfoStrs = getSettingsManager().getLoopImagesWidgetDirInfo(appWidgetId); + List dirInfos = new ArrayList(); + for(String info: dirInfoStrs){ + dirInfos.add(getDirInfoFromString(context, info)); + } + return dirInfos; + } + + static public void deleteDirInfo(int appWidgetId) { + getSettingsManager().deleteLoopImagesWidgetInfo(appWidgetId); + } + + public static void init(Context context){ + if(settingsMgr == null) { + settingsMgr = SettingsManager.instance(); + } + + if(accountManager == null) { + accountManager = new AccountManager(context); + } + + if(accountList == null) { + accountList = accountManager.getAccountList(); + } + + if(signatureAccount == null) { + signatureAccount = new HashMap(); + for(Account taccount: accountList){ + signatureAccount.put(taccount.getSignature(), taccount); + } + } + + if(dataManagerMap == null){ + dataManagerMap = new HashMap(); + } + } + + public static DataManager getDataManager(Account account){ + if(dataManagerMap == null){ + dataManagerMap = new HashMap(); + } + if(!dataManagerMap.containsKey(account.getSignature())){ + dataManagerMap.put(account.getSignature(), new DataManager(account)); + } + return dataManagerMap.get(account.getSignature()); + } + + public LinePageIndicator getIndicator(){ + return mIndicator; + } + + + @Override + public void onDestroy() { + super.onDestroy(); + existActivity = null; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if(existActivity != null){ + existActivity.finish(); + } + existActivity = this; + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED); + + setContentView(R.layout.loop_images_configure_activity_layout); + + init(getApplicationContext()); + + // Find the widget id from the intent. + Intent intent = getIntent(); + + Bundle extras = intent.getExtras(); + if (extras != null) { + mAppWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + String dirInfoStr = extras.getString(LoopImagesWidget.DIR_INFO); + String fileName = extras.getString(LoopImagesWidget.IMAGE_NAME); + if(dirInfoStr != null && dirInfoStr.length() > 0 && fileName != null && fileName.length() > 0){ + hasImage = true; + } + } + + // If this activity was started with an intent without an app widget ID, finish with an error. + if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish(); + return; + } + + mViewPager = (ViewPager) findViewById(R.id.loopimages_configure_pager); + FragmentManager fm = getSupportFragmentManager(); + mViewPager.setAdapter(new LoopImagesConfigAdapter(fm)); + mViewPager.setOffscreenPageLimit(6); + + mIndicator = (LinePageIndicator) findViewById(R.id.loopimages_configure_indicator); + mIndicator.setViewPager(mViewPager); + mIndicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { + + @Override + public void onPageScrollStateChanged(int scrollState) {} + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPosition = position; + } + + @Override + public void onPageSelected(int page){} + }); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) { + super.onSaveInstanceState(outState, outPersistentState); + } + + @Override + public void onBackPressed() { + if (mCurrentPosition == 0) { + setResult(RESULT_CANCELED); + super.onBackPressed(); + } else { + // navigate to previous page when press back button + mCurrentPosition -= 1; + mIndicator.setCurrentItem(mCurrentPosition); + } + } + + class LoopImagesConfigAdapter extends FragmentStatePagerAdapter { + + public LoopImagesConfigAdapter(FragmentManager fm) { + super(fm); + } + + // This method controls which fragment should be shown on a specific screen. + @Override + public Fragment getItem(int position) { + // Assign the appropriate screen to the fragment object, based on which screen is displayed. + switch (position) { + case 0: +// return new ChosenLibraryFragment(); + if(hasImage) { + return new ShowImageFragment(); + }else{ + return new HowToDownloadFragment(); + } + case 1: + if(hasImage){ +// mIndicator.setVisibility(View.VISIBLE); + return new HowToDownloadFragment(); + }else{ + return new ChosenLibraryFragment(); + } + case 2: + if(hasImage){ + return new ChosenLibraryFragment(); + }else{ + return null; + } + default: + return null; + } + } + + @Override + public int getCount() { + if(hasImage){ + return 3; + } + return 2; + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetService.java b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetService.java new file mode 100644 index 000000000..69e1326a5 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/LoopImagesWidgetService.java @@ -0,0 +1,891 @@ +package com.seafile.seadroid2.loopimages; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.ImageDecoder; +import android.graphics.Matrix; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.SystemClock; +import android.support.annotation.RequiresApi; +import android.support.v4.app.NotificationCompat; +import android.telephony.mbms.FileInfo; +import android.util.Log; +import android.view.SurfaceControl; +import android.widget.ImageView; +import android.widget.RemoteViews; + +import com.google.common.collect.Lists; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.cameraupload.CameraSyncAdapter; +import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.data.DatabaseHelper; +import com.seafile.seadroid2.data.DirentCache; +import com.seafile.seadroid2.data.SeafCachedFile; +import com.seafile.seadroid2.data.SeafDirent; +import com.seafile.seadroid2.gallery.Image; +import com.seafile.seadroid2.transfer.DownloadTask; +import com.seafile.seadroid2.transfer.DownloadTaskInfo; +import com.seafile.seadroid2.transfer.DownloadTaskManager; +import com.seafile.seadroid2.transfer.TaskState; +import com.seafile.seadroid2.transfer.TransferService; +import com.seafile.seadroid2.transfer.UploadTaskInfo; +import com.seafile.seadroid2.util.ConcurrentAsyncTask; +import com.seafile.seadroid2.util.Utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Random; +import java.util.Set; + + +public class LoopImagesWidgetService extends Service { + + private static final int ALARM_DURATION = 1 * 60 * 1000; + private static final int UPDATE_DURATION = 5 * 1000; + private static final int UPDATE_MESSAGE = 1000; + private static final int CHECKT_DOWNLOAD_STATE = 1001; + private static final int CHECKT_DOWNLOAD_STATE_DURATION = 5*1000; + private static final int UPDATE_IMAGE_DELAY = 60 * 60 * 1000; + private static final int ONCE_UPLOAD_IMAGE_NUM = 3; + private static final int ONCE_REMOVE_IMAGE_NUM = 3*ONCE_UPLOAD_IMAGE_NUM; + private static final int LEFT_PREVIOUS_MAX_IMAGES_NUM = 100; + private static final int LEFT_FOLLOW_UP_MAX_IMAGES_NUM = 1000; + private static final int STOP_DOWNLOAD_TIMES = 3*LEFT_FOLLOW_UP_MAX_IMAGES_NUM; + public static final String UPDATE_WIDGETS_KEY = "update_widgets_key"; + public static final int UPDATE_ALL_WIDGETS = -1; + public static final int UPDATE_NONE_WIDGETS = -2; + public static final String DELETE_WIDGETS_KEY = "delete_widgets_key"; + public static final int DELETE_ALL_WIDGETS = -1; + public static final int DELETE_NONE_WIDGETS = -2; + public static final String UPDATE_IMAGE_INFO_SIGNAL = "update_image_info_signal"; + public static final String DELETE_IMAGE_INFO_SIGNAL = "delete_image_info_signal"; + public static final String DELAY_UPDATE_ALL_SIGNAL = "delay_update_signal"; + private static final String DEBUG_TAG = "LoopImagesWidgetService"; + + private Object imageInfosLock = new Object(); + private Map> imageInfos; + private Map queues; + private LinkedList tasksInProgress = Lists.newLinkedList(); + private Map shouldDownloads; + private Map caches; + private Random rand = new Random(); + + private boolean isUpdatingInfo = false; + private Object updatingInfoLock = new Object(); + + private int imageInfoDelayTimes = 0; + + private static final String[] FILE_SUFIX = new String[]{ + "gif", + "png", + "bmp", + "jpeg", + "jpg", + "webp" + }; + + private TransferService txService = null; + + private UpdateHandler updateHandler; + + ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + TransferService.TransferBinder binder = (TransferService.TransferBinder) service; + synchronized (LoopImagesWidgetService.this) { + txService = binder.getService(); + } + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + + synchronized (LoopImagesWidgetService.this) { + txService = null; + } + } + }; + + private synchronized void startTransferService() { + if (txService != null) + return; + + Intent bIntent = new Intent(getApplicationContext(), TransferService.class); + getApplicationContext().bindService(bIntent, mConnection, Context.BIND_AUTO_CREATE); + } + + private synchronized void stopTransferService(){ + if(mConnection == null){ + return; + } + getApplicationContext().unbindService(mConnection); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void sendAlaramMessage(){ + AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent alarmIntent = new Intent(getBaseContext(), LoopImagesWidgetService.class); + PendingIntent pendingIntent = PendingIntent.getService(getBaseContext(), 0, + alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + ALARM_DURATION, pendingIntent); + } + + private void addUpdateInfoTask(int updateSignal, int deleteSignal){ + synchronized (updatingInfoLock){ + if(!isUpdatingInfo) { + isUpdatingInfo = true; + ConcurrentAsyncTask.execute(new UpdateImageInfoTask(updateSignal, deleteSignal)); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(DEBUG_TAG, "onStartCommand"); + + int updateAppWidgetSignal = UPDATE_NONE_WIDGETS; + if(intent!=null) { + updateAppWidgetSignal = intent.getIntExtra(UPDATE_IMAGE_INFO_SIGNAL, UPDATE_NONE_WIDGETS); + } + if(updateAppWidgetSignal != UPDATE_NONE_WIDGETS){ + addUpdateInfoTask(updateAppWidgetSignal, DELETE_NONE_WIDGETS); + return START_STICKY; + } + + int deleteAppWidgetSignal = DELETE_NONE_WIDGETS; + if(intent != null){ + deleteAppWidgetSignal = intent.getIntExtra(DELETE_IMAGE_INFO_SIGNAL, DELETE_NONE_WIDGETS); + } + if(deleteAppWidgetSignal != DELETE_NONE_WIDGETS){ + addUpdateInfoTask(UPDATE_NONE_WIDGETS, deleteAppWidgetSignal); + return START_STICKY; + } + + boolean delayUpdateAll = false; + if(intent != null) { + delayUpdateAll = intent.getBooleanExtra(DELAY_UPDATE_ALL_SIGNAL, false); + } + if(delayUpdateAll){ + AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent alarmIntent = new Intent(getBaseContext(), LoopImagesWidgetService.class); + alarmIntent.putExtra(UPDATE_IMAGE_INFO_SIGNAL, UPDATE_ALL_WIDGETS); + PendingIntent pendingIntent = PendingIntent.getService(getBaseContext(), 0, + alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + 60*1000, pendingIntent); + return START_STICKY; + } + + if(imageInfoDelayTimes < UPDATE_IMAGE_DELAY / ALARM_DURATION){ + ++imageInfoDelayTimes; + }else{ + imageInfoDelayTimes = 0; + } + + if(imageInfoDelayTimes == 1){ + addUpdateInfoTask(UPDATE_ALL_WIDGETS, DELETE_NONE_WIDGETS); + } + + sendAlaramMessage(); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopTransferService(); + } + + @Override + public void onCreate() { + super.onCreate(); + + Log.d(DEBUG_TAG, "onCreate"); + + if(updateHandler == null) { + updateHandler = new UpdateHandler(); + } + + Message message = updateHandler.obtainMessage(); + message.what = UPDATE_MESSAGE; + updateHandler.sendMessageDelayed(message, UPDATE_DURATION); + + startTransferService(); + createNotificationChannel(); + sendAlaramMessage(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= 26) { + String CHANNEL_ID = "seafile_channel"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + "seafile notification", + NotificationManager.IMPORTANCE_DEFAULT); + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("") + .setContentText("").build(); + startForeground(1, notification); + } + } + + private void downloadImages(int appWidgetId, ImageInfo imageInfo){ + File file = imageInfo.getFile(); + SeafDirent seafDirent = imageInfo.getSeafDirent(); + String filePath = imageInfo.getFilePath(); + if(seafDirent == null || filePath == null){ + return; + } + if(file != null && file.exists()){ + if(getFileSize(imageInfo.getFile()) == seafDirent.getFileSize()) { + queues.get(appWidgetId).push(imageInfo); + return; + }else{ + imageInfo.deleteStorage(); + } + } + int taskID = txService.addDownloadTask(imageInfo.getDirInfo().getAccount(), + imageInfo.getDirInfo().getRepoName(), + imageInfo.getDirInfo().getRepoId(), + filePath, + seafDirent.size); + tasksInProgress.add(new TaskInfo(taskID, appWidgetId, imageInfo)); + } + + private long getFileSize(File file) { + long size = 0; + try { + if (file.exists()) { + FileInputStream fis = null; + fis = new FileInputStream(file); + size = fis.available(); + } + }catch (Exception e){ + return 0; + } + return size; + } + + private void removeLeftImages(int appWidgetId){ + int removeNum = ONCE_REMOVE_IMAGE_NUM; + while (removeNum > 0 && queues.get(appWidgetId).getRemoveCount() > LEFT_PREVIOUS_MAX_IMAGES_NUM) { + --removeNum; + ImageInfo deleteItem = queues.get(appWidgetId).popRemoveItem(); + if(deleteItem == null) { + continue; + } + deleteItem.deleteStorage(); + String filePath = deleteItem.getFilePath(); + if (filePath != null) { + Log.d(DEBUG_TAG, "Delete file that exceed storage space: " + filePath + "!!!"); +// Utils.utilsLogInfo(true, "Delete file that exceed storage space: " + filePath + "!!!"); + } + } + } + + private void checkDownloadTasks(){ + Log.d(DEBUG_TAG, "checkDownloadTasks"); + int removeNum = 0; + for (TaskInfo taskInfo: tasksInProgress) { + DownloadTaskInfo info = txService.getDownloadTaskInfo(taskInfo.taskID); + if(info == null){ + ++removeNum; + continue; + } + ImageInfo item = taskInfo.imageInfo; + if (info.err != null) { + txService.removeDownloadTask(taskInfo.taskID); + ++removeNum; + item.deleteStorage(); + Utils.utilsLogInfo(true, "=======Task " + Integer.toString(taskInfo.taskID) + " failed to download " + info.localFilePath + "!!!"); + continue; + } + if (info.state == TaskState.INIT || info.state == TaskState.TRANSFERRING) { + break; + } + ++removeNum; + SeafDirent seafDirent = item.getSeafDirent(); + if(seafDirent == null){ + continue; + } + boolean haveSameSize = getFileSize(item.getFile()) + == seafDirent.getFileSize(); + if (info.state == TaskState.FINISHED && haveSameSize) { + queues.get(taskInfo.appWidgetId).push(taskInfo.imageInfo); +// if (queues.get(taskInfo.appWidgetId).getCount() >= LEFT_FOLLOW_UP_MAX_IMAGES_NUM) { +// shouldDownloads.put(taskInfo.appWidgetId, false); +// } + } else { + item.deleteStorage(); + } + txService.removeDownloadTask(taskInfo.taskID); + } + while(removeNum > 0 && !tasksInProgress.isEmpty()){ + --removeNum; + tasksInProgress.removeFirst(); + } + } + + private RemoteViews setRemoteViewOnClickActivity(RemoteViews views, int appWidgetId, String dirInfo, String imageName){ + Intent activityIntent = new Intent(getApplicationContext(), LoopImagesWidgetConfigureActivity.class); + activityIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + activityIntent.putExtra(LoopImagesWidget.DIR_INFO, dirInfo); + activityIntent.putExtra(LoopImagesWidget.IMAGE_NAME, imageName); + PendingIntent widgetIntent = PendingIntent.getActivity(getApplicationContext(), appWidgetId, activityIntent, PendingIntent.FLAG_CANCEL_CURRENT); + views.setOnClickPendingIntent(R.id.loopimages_widget_relative_layout, widgetIntent); + return views; + } + + private RemoteViews getDefalutRemoteViews(int appWidgetId){ + Context context = getApplicationContext(); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.loop_images_widget); + views.setImageViewResource(R.id.loopimages_imageview, R.drawable.rem); + views = setRemoteViewOnClickActivity(views, appWidgetId, null, null); + return views; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private Map updateWidget() { + Map results = new HashMap(); + synchronized (imageInfosLock) { + checkDownloadTasks(); +// Utils.utilsLogInfo(true, "=======Update Widget."); + Context context = getApplicationContext(); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int appWidgetIds[] = appWidgetManager.getAppWidgetIds(new ComponentName(context, LoopImagesWidget.class)); + if (imageInfos == null || queues == null || appWidgetIds.length <= 0) { + addUpdateInfoTask(UPDATE_ALL_WIDGETS, DELETE_NONE_WIDGETS); + Message message = updateHandler.obtainMessage(); + message.what = UPDATE_MESSAGE; + updateHandler.sendMessageDelayed(message, UPDATE_DURATION); + return results; + } + int n = appWidgetIds.length; + for (int i = 0; i < n; ++i) { + int appWidgetId = appWidgetIds[i]; + if (i >= imageInfos.size() || i >= queues.size()) { + break; + } + List imageInfo = imageInfos.get(appWidgetId); + int m = imageInfo.size(); + if (m <= 0) { + addUpdateInfoTask(appWidgetId, DELETE_NONE_WIDGETS); + RemoteViews views = getDefalutRemoteViews(appWidgetId); + results.put(appWidgetId, views); +// appWidgetManager.updateAppWidget(appWidgetId, views); + continue; + } + boolean enableDownload = Utils.isWiFiOn() || LoopImagesWidgetConfigureActivity.getDataPlanAllowed(appWidgetId); + if (Utils.isNetworkOn() && enableDownload && tasksInProgress.size() < ONCE_UPLOAD_IMAGE_NUM * 2 * n) { + if (queues.get(appWidgetId).getCount() < LEFT_FOLLOW_UP_MAX_IMAGES_NUM && shouldDownloads.get(appWidgetId) == 0) { + Log.d(DEBUG_TAG, "Download new images."); + int addTaskNum = ONCE_UPLOAD_IMAGE_NUM; + while (addTaskNum > 0) { + downloadImages(appWidgetId, imageInfo.get(rand.nextInt(m))); + --addTaskNum; + } + } + if(queues.get(appWidgetId).getCount() >= LEFT_FOLLOW_UP_MAX_IMAGES_NUM || shouldDownloads.get(appWidgetId) != 0){ + shouldDownloads.put(appWidgetId, (shouldDownloads.get(appWidgetId)+1)%STOP_DOWNLOAD_TIMES); + if(shouldDownloads.get(appWidgetId) == 0){ + queues.get(appWidgetId).removeAll(); + } + } + } + if (queues.get(appWidgetId).isEmpty()) { + RemoteViews views = getDefalutRemoteViews(appWidgetId); + results.put(appWidgetId, views); +// appWidgetManager.updateAppWidget(appWidgetId, views); + continue; + } + boolean flag = false; + while (!queues.get(appWidgetId).isEmpty()) { + ImageInfo item = queues.get(appWidgetId).next(); + if(item == null){ + queues.get(appWidgetId).removePreviousItem(); + } + Bitmap image = item.getBitMap(); + if (image == null) { + item.deleteStorage(); + queues.get(appWidgetId).removePreviousItem(); + continue; + } + Log.d(DEBUG_TAG, "Set new images."); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.loop_images_widget); + views.setImageViewBitmap(R.id.loopimages_imageview, image); + SeafDirent seafDirent = item.getSeafDirent(); + if(seafDirent == null){ + views = setRemoteViewOnClickActivity(views, appWidgetId,null, null); + }else { + views = setRemoteViewOnClickActivity(views, appWidgetId, item.getDirInfo().toString(), seafDirent.name); + } + results.put(appWidgetId, views); +// appWidgetManager.updateAppWidget(appWidgetId, views); + flag = true; + break; + } + if (!flag) { + RemoteViews views = getDefalutRemoteViews(appWidgetId); + results.put(appWidgetId, views); +// appWidgetManager.updateAppWidget(appWidgetId, views); + } + removeLeftImages(appWidgetId); + Log.d(DEBUG_TAG, "=======Loopimages widget "+appWidgetId+" queue info: "+queues.get(appWidgetId).getCount()+" images, "+queues.get(appWidgetId).getRemoveCount()+" remain images."); +// Utils.utilsLogInfo(true, "=======Loopimages widget "+appWidgetId+" queue info: "+queues.get(appWidgetId).getCount()+" images, "+queues.get(appWidgetId).getRemoveCount()+" remain images."); + } + } + Message message = updateHandler.obtainMessage(); + message.what = UPDATE_MESSAGE; + updateHandler.sendMessageDelayed(message, UPDATE_DURATION); + return results; + } + + private void updateImageInfo(int updateWidgetSignal){ + if(updateWidgetSignal == UPDATE_NONE_WIDGETS){ + return; + } + synchronized (imageInfosLock) { + Context context = getApplicationContext(); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + + if(caches == null){ + caches = new HashMap(); + } + + if(queues == null){ + queues = new HashMap(); + } + if (imageInfos == null || shouldDownloads == null) { + updateWidgetSignal = UPDATE_ALL_WIDGETS; + imageInfos = new HashMap>(); + shouldDownloads = new HashMap(); + } + + Map notUsedDir = new HashMap(); + for(String dirStr: caches.keySet()){ + notUsedDir.put(dirStr, true); + } + + int appWidgetIds[]; + if (updateWidgetSignal != UPDATE_ALL_WIDGETS) { + appWidgetIds = new int[]{updateWidgetSignal}; + } else { + appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, LoopImagesWidget.class)); + } + + Map> dirImageInfoMapping = new HashMap>(); + + Map> widgetDirInfos = new HashMap>(); + for (int appWidgetId : appWidgetIds) { + widgetDirInfos.put(appWidgetId, LoopImagesWidgetConfigureActivity.getDirInfo(getApplicationContext(), appWidgetId)); + } + for (int i = 0; i < appWidgetIds.length; ++i) { + boolean haveUpdated = false; + int appWidgetId = appWidgetIds[i]; + if(!queues.containsKey(appWidgetId)){ + queues.put(appWidgetId, new LazyQueue()); + } + List dirInfos = widgetDirInfos.get(appWidgetId); + List nImageInfo = Lists.newArrayList(); + for (DirInfo info : dirInfos) { + if (dirImageInfoMapping.containsKey(info.toString())) { + nImageInfo.addAll(dirImageInfoMapping.get(info.toString())); + continue; + } + String dirID = LoopImagesWidgetConfigureActivity.getDataManager(info.getAccount()).getDirID(info.getRepoId(), info.getDirPath()); + notUsedDir.put(info.toString(), false); + + if(info.getDirId() != dirID) { + caches.remove(info.toString()); + info.setDirId(dirID); + haveUpdated = true; + } + AutoCleanDirentCache cache = null; + if(caches.containsKey(info.toString())){ + cache = caches.get(info.toString()); + }else { + try { + List seafDirents = LoopImagesWidgetConfigureActivity.getDataManager(info.getAccount()).getCachedDirents(info.getRepoId(), info.getDirPath()); + cache = new AutoCleanDirentCache("LoopImages-dirent-" + dirID, seafDirents); + } catch (IOException e) { + Log.e(DEBUG_TAG, "Error to save cache.", e); + } + } + if(cache == null){ + continue; + } + caches.put(info.toString(), cache); + List files = Lists.newArrayList(); + for(int k=0;k dirInfoStrs = new ArrayList(); + for(DirInfo dirInfo: dirInfos){ + dirInfoStrs.add(dirInfo.toString()); + } + LoopImagesWidgetConfigureActivity.getSettingsManager().setLoopImagesWidgetDirInfo(appWidgetId, dirInfoStrs); + } + } + if(updateWidgetSignal == UPDATE_ALL_WIDGETS) { + for (String dirStr : notUsedDir.keySet()) { + if (notUsedDir.get(dirStr)) { + notUsedDir.remove(dirStr); + } + } + } + } + } + + public void deleteAppWidgetInfo(int deleteAppWidgetSignal){ + if(deleteAppWidgetSignal == DELETE_NONE_WIDGETS){ + return; + } + synchronized (imageInfosLock) { + Context context = getApplicationContext(); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int appWidgetIds[]; + if (deleteAppWidgetSignal != DELETE_ALL_WIDGETS) { + appWidgetIds = new int[]{deleteAppWidgetSignal}; + } else { + appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, LoopImagesWidget.class)); + } + for(int appWidgetId: appWidgetIds) { + if (imageInfos != null && imageInfos.containsKey(appWidgetId)) { + imageInfos.remove(appWidgetId); + } + if (queues != null && queues.containsKey(appWidgetId)) { + queues.remove(appWidgetId); + } + if (shouldDownloads != null && shouldDownloads.containsKey(appWidgetId)) { + shouldDownloads.remove(appWidgetId); + } + } + } + } + + protected class TaskInfo{ + public int taskID; + public int appWidgetId; + public ImageInfo imageInfo; + + public TaskInfo(int taskID, int appWidgetId, ImageInfo imageInfo){ + this.taskID = taskID; + this.appWidgetId = appWidgetId; + this.imageInfo = imageInfo; + } + } + + protected class LazyQueue{ + private LinkedList data; + private LinkedList reserve; + private LinkedList remove; + + public LazyQueue() { + data = Lists.newLinkedList(); + reserve = Lists.newLinkedList(); + remove = Lists.newLinkedList(); + } + + public void push(ImageInfo info){ + data.add(info); + } + + public ImageInfo popRemoveItem(){ + if(remove.size() <= 0){ + return null; + } + return remove.pop(); + } + + public int getCount(){ + return data.size() + reserve.size(); + } + + public int getRemoveCount(){ + return remove.size(); + } + + public int getTotalCount(){ + return getCount() + getRemoveCount(); + } + + public boolean isEmpty(){ + return data.isEmpty() && reserve.isEmpty() && remove.isEmpty(); + } + + public ImageInfo next(){ + if(data.size() <= 0 && reserve.size() <= 0 && remove.size() <= 0){ + return null; + } + if(data.size() <= 0 && reserve.size() > 0){ + ImageInfo res = reserve.pop(); + reserve.add(res); + return res; + } + if(data.size() <= 0 && reserve.size() <= 0){ + ImageInfo res = remove.pop(); + remove.add(res); + return res; + } + ImageInfo res = data.pop(); + reserve.add(res); + return res; + } + + public void removePreviousItem(){ + if(reserve.size() > 0){ + remove.add(reserve.removeLast()); + } + } + + public void removeAll(){ + remove.addAll(data); + remove.addAll(reserve); + data.clear(); + reserve.clear(); + } + + public ImageInfo back(){ + if(data.size() <= 0){ + return null; + } + return data.getLast(); + } + } + + protected final class UpdateHandler extends Handler { + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_MESSAGE: + ConcurrentAsyncTask.execute(new UpdateWdigetTask()); + break; + default: + break; + } + } + } + + protected final class UpdateWdigetTask extends AsyncTask, Map> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected Map doInBackground(Integer... integers) { + Map views = updateWidget(); + return views; + } + + @Override + protected void onProgressUpdate(Map... values) { + super.onProgressUpdate(values); + } + + @Override + protected void onPostExecute(Map value) { + super.onPostExecute(value); + if(value == null) { + return; + } + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext()); + for (int appWidgetId : value.keySet()) { + RemoteViews view = value.get(appWidgetId); + if(view != null) { + appWidgetManager.updateAppWidget(appWidgetId, view); + } + } + } + } + + protected class UpdateImageInfoTask extends AsyncTask{ + private int updateSignal; + private int deleteSignal; + + public UpdateImageInfoTask(int updateSignal, int deleteSignal){ + this.updateSignal = updateSignal; + this.deleteSignal = deleteSignal; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected Integer doInBackground(Integer... integers) { + if(updateSignal != UPDATE_NONE_WIDGETS){ + updateImageInfo(updateSignal); + } + if(deleteSignal != DELETE_NONE_WIDGETS){ + deleteAppWidgetInfo(deleteSignal); + } + synchronized (updatingInfoLock){ + isUpdatingInfo = false; + } + return 0; + } + + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + } + + @Override + protected void onPostExecute(Integer value) { + super.onPostExecute(value); + } + } + + class ImageInfo{ + private DirInfo dirInfo; + private int index; + private DirentCache cache; + private static final int MAX_BITMAP_EDGE = 1024; + + public ImageInfo(DirInfo dirInfo, DirentCache cache, int index){ + this.dirInfo = dirInfo; + this.cache = cache; + this.index = index; + } + + public DirInfo getDirInfo() { + return dirInfo; + } + + public SeafDirent getSeafDirent() { + if(this.cache == null){ + return null; + } + return cache.get(index); + } + + public String getFilePath(){ + SeafDirent seafDirent = getSeafDirent(); + if(seafDirent == null){ + return null; + } + return Utils.pathJoin(getDirInfo().getDirPath(), seafDirent.name); + } + + public File getFile(){ + DataManager dataManager = new DataManager(getDirInfo().getAccount()); + String filePath = getFilePath(); + if(filePath == null){ + return null; + } + File file = dataManager.getLocalRepoFile(getDirInfo().getRepoName(), getDirInfo().getRepoId(), filePath); + return file; + } + + public boolean exist(){ + return getFile() != null; + } + + public Bitmap getBitMap(){ + File file = getFile(); + if(file == null || !file.exists()){ + return null; + } + Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); + if(bitmap == null){ + return null; + } + if(bitmap.getHeight() > MAX_BITMAP_EDGE || bitmap.getWidth() > MAX_BITMAP_EDGE){ + float scale = MAX_BITMAP_EDGE/(float)Math.max(bitmap.getHeight(), bitmap.getWidth()); + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + return bitmap; + } + + public boolean deleteStorage(){ + File file = getFile(); + if(file == null){ + return true; + } + return file.delete(); + } + } + + protected class AutoCleanDirentCache extends DirentCache{ + public AutoCleanDirentCache(String name) throws IOException { + super(name); + } + + public AutoCleanDirentCache(String name, List caches) throws IOException{ + super(name, caches); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + super.delete(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/RemoteLibrarySelectionActivity.java b/app/src/main/java/com/seafile/seadroid2/loopimages/RemoteLibrarySelectionActivity.java new file mode 100644 index 000000000..d8ea21f22 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/RemoteLibrarySelectionActivity.java @@ -0,0 +1,838 @@ +package com.seafile.seadroid2.loopimages; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.google.common.collect.Lists; +import com.seafile.seadroid2.SeafConnection; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.SettingsManager; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.AccountManager; +import com.seafile.seadroid2.avatar.Avatar; +import com.seafile.seadroid2.avatar.AvatarManager; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.cameraupload.CloudLibraryAccountAdapter; +import com.seafile.seadroid2.cameraupload.CloudLibraryAdapter; +import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.data.SeafDirent; +import com.seafile.seadroid2.data.SeafRepo; +import com.seafile.seadroid2.ui.NavContext; +import com.seafile.seadroid2.ui.activity.BaseActivity; +import com.seafile.seadroid2.ui.activity.SeafilePathChooserActivity; +import com.seafile.seadroid2.ui.adapter.DirentsAdapter; +import com.seafile.seadroid2.ui.dialog.PasswordDialog; +import com.seafile.seadroid2.ui.dialog.TaskDialog; +import com.seafile.seadroid2.util.ConcurrentAsyncTask; +import com.seafile.seadroid2.util.SystemSwitchUtils; +import com.seafile.seadroid2.util.Utils; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; + +public class RemoteLibrarySelectionActivity extends BaseActivity { + + public static final String DEBUG_TAG = "RemoteLibrarySelectionActivity"; + + public static final String PASSWORD_DIALOG_FRAGMENT_TAG = "passwordDialogFragmentTag"; + public static final String ONLY_SHOW_WRITABLE_REPOS = "onlyShowWritableRepos"; + public static final String SHOW_ENCRYPTED_REPOS = "showEncryptedRepos"; + public static final String ENCRYPTED_REPO_ID = "encryptedRepoId"; + + public static final String DATA_REPO_ID = "repoID"; + public static final String DATA_REPO_NAME = "repoNAME"; + public static final String DATA_DIR = "dir"; + public static final String DATA_ACCOUNT = "account"; + + private static final int STEP_CHOOSE_ACCOUNT = 1; + private static final int STEP_CHOOSE_REPO = 2; + private static final int STEP_CHOOSE_DIR = 3; + private int mStep = 1; + + private CloudLibraryAccountAdapter mAccountAdapter; + private CloudLibraryAdapter mReposAdapter; + private DirentsAdapter mDirentsAdapter; + private AccountManager mAccountManager; + private DataManager mDataManager; + private NavContext mNavContext; + private Account mAccount; + private LoadAccountsTask mLoadAccountsTask; + private LoadReposTask mLoadReposTask; + private LoadDirTask mLoadDirTask; + private AvatarManager avatarManager; + + private RelativeLayout mUpLayout; + private TextView mCurrentFolderText; + private TextView mEmptyText, mErrorText; + private ImageView mRefreshBtn; + private View mProgressContainer, mListContainer; + private ListView mFoldersListView; + private Button mDoneButton; +// private Cursor mCursor; + private String mCurrentDir; + + private boolean canChooseAccount; + private boolean onlyShowWritableRepos; + private String encryptedRepoId; + + /** only show repo list for camera upload */ + private boolean isOnlyChooseRepo; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.loop_images_widget_remote_library_configure); + + Intent intent = getIntent(); + avatarManager = new AvatarManager(); + Account account = intent.getParcelableExtra("account"); + if (account == null) { + canChooseAccount = true; + } else { + mAccount = account; + } + onlyShowWritableRepos = intent.getBooleanExtra(ONLY_SHOW_WRITABLE_REPOS, true); + encryptedRepoId = intent.getStringExtra(ENCRYPTED_REPO_ID); + isOnlyChooseRepo = false; + + mFoldersListView = (ListView) findViewById(R.id.loopimages_selection_lv); + mFoldersListView.setFastScrollEnabled(true); + mUpLayout = (RelativeLayout) findViewById(R.id.loopimages_selection_up_layout); + mCurrentFolderText = (TextView) findViewById(R.id.loopimages_selection_current_directory_txt); + mEmptyText = (TextView) findViewById(R.id.loopimages_selection_empty_msg); + mErrorText = (TextView) findViewById(R.id.loopimages_selection_error_msg); + mRefreshBtn = (ImageView) findViewById(R.id.loopimages_selection_refresh_iv); + mProgressContainer = findViewById(R.id.loopimages_selection_progress_container); + mListContainer = findViewById(R.id.loopimages_selection_list_container); + mDoneButton = (Button)findViewById(R.id.loopimages_remote_library_done_btn); + mRefreshBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + refreshList(true); + } + }); + + mUpLayout.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + try { + stepBack(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + }); + + mFoldersListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + onListItemClick(parent, position, id); + } + }); + + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(); + intent.putExtra(SeafilePathChooserActivity.DATA_REPO_NAME, getNavContext().getRepoName()); + intent.putExtra(SeafilePathChooserActivity.DATA_REPO_ID, getNavContext().getRepoID()); + intent.putExtra(SeafilePathChooserActivity.DATA_DIR, getNavContext().getDirID()); + intent.putExtra(SeafilePathChooserActivity.DATA_DIRECTORY_PATH, getNavContext().getDirPath()); + intent.putExtra(SeafilePathChooserActivity.DATA_ACCOUNT, mAccount); + + setResult(RESULT_OK, intent); + finish(); + } + }); + + if (canChooseAccount) { + chooseAccount(); + } else { + chooseRepo(); + } + + } + + @Override + public void onResume() { + super.onResume(); + loadAvatarUrls(48); + } + + private void refreshList(final boolean forceRefresh) { + switch (mStep) { + case STEP_CHOOSE_ACCOUNT: + if (mLoadAccountsTask != null && mLoadAccountsTask.getStatus() != AsyncTask.Status.FINISHED) { + return; + } else { + chooseAccount(false); + break; + } + case STEP_CHOOSE_REPO: + if (mLoadReposTask != null && mLoadReposTask.getStatus() != AsyncTask.Status.FINISHED) { + return; + } else { + chooseRepo(forceRefresh); + break; + } + case STEP_CHOOSE_DIR: + if (mLoadDirTask != null && mLoadDirTask.getStatus() != AsyncTask.Status.FINISHED) { + return; + } else { + SeafRepo repo = getDataManager().getCachedRepoByID(getNavContext().getRepoID()); + if (repo.encrypted && !getDataManager().getRepoPasswordSet(repo.id)) { + String password = getDataManager().getRepoPassword(repo.id); + showPasswordDialog(repo.name, repo.id, + new TaskDialog.TaskDialogListener() { + @Override + public void onTaskSuccess() { + chooseRepo(forceRefresh); + } + } , password); + } + chooseDir(forceRefresh); + break; + } + } + } + + public void onListItemClick(final View v, final int position, final long id) { + NavContext nav = getNavContext(); + SeafRepo repo = null; + + if (mStep == STEP_CHOOSE_REPO) { + repo = getReposAdapter().getItem(position); + //mCurrentFolderText.setText(nav.getRepoName()); + } else if (mStep == STEP_CHOOSE_DIR) { + repo = getDataManager().getCachedRepoByID(nav.getRepoID()); + + } + + if (repo != null) { + if (repo.encrypted && !getDataManager().getRepoPasswordSet(repo.id)) { + String password = getDataManager().getRepoPassword(repo.id); + showPasswordDialog(repo.name, repo.id, new TaskDialog.TaskDialogListener() { + @Override + public void onTaskSuccess() { + onListItemClick(v, position, id); + } + }, password); + + return; + } + } + + switch (mStep) { + case STEP_CHOOSE_ACCOUNT: + setAccount(getAccountAdapter().getItem(position)); + mCurrentDir = mAccount.getDisplayName(); + setCurrentDirText(mCurrentDir); + chooseRepo(); + mDoneButton.setVisibility(View.GONE); + break; + case STEP_CHOOSE_REPO: + if (!isOnlyChooseRepo) { + nav.setRepoName(repo.name); + nav.setRepoID(repo.id); + nav.setDir("/", repo.root); + chooseDir(); + } + mCurrentDir = getString(R.string.settings_cuc_remote_lib_repo, repo.name); + setCurrentDirText(mCurrentDir); + SeafRepo seafRepo = getReposAdapter().getItem(position); + onRepoSelected(mAccount, seafRepo); + mDoneButton.setVisibility(View.VISIBLE); + break; + case STEP_CHOOSE_DIR: + SeafDirent dirent = getDirentsAdapter().getItem(position); + mCurrentDir += "/" + dirent.name; + setCurrentDirText(mCurrentDir); + + if (dirent.type == SeafDirent.DirentType.FILE) { + return; + } + + nav.setDir(Utils.pathJoin(nav.getDirPath(), dirent.name), dirent.id); + refreshDir(); + mDoneButton.setVisibility(View.VISIBLE); + break; + } + } + + private void onRepoSelected(Account account, SeafRepo seafRepo) { + getReposAdapter().setSelectedRepo(seafRepo); + getReposAdapter().notifyDataSetChanged(); + } + + private void stepBack() { + stepBack(false); + } + + private void stepBack(boolean cancelIfFirstStep) { + switch (mStep) { + case STEP_CHOOSE_ACCOUNT: + if (cancelIfFirstStep) { + finish(); + } + mUpLayout.setVisibility(View.INVISIBLE); + break; + case STEP_CHOOSE_REPO: + if (canChooseAccount) { + mCurrentDir = getString(R.string.settings_cuc_remote_lib_account); + setCurrentDirText(mCurrentDir); + chooseAccount(false); + } else if (cancelIfFirstStep) { + finish(); + } + break; + case STEP_CHOOSE_DIR: + if (getNavContext().isRepoRoot()) { + mCurrentDir = getAccountManager().getCurrentAccount().getEmail(); + setCurrentDirText(mCurrentDir); + chooseRepo(); + } else { + String path = getNavContext().getDirPath(); + mCurrentDir = getNavContext().getRepoName() + Utils.getParentPath(path); + setCurrentDirText(mCurrentDir); + getNavContext().setDir(Utils.getParentPath(path), null); + refreshDir(); + } + break; + } + } + + private void showPasswordDialog() { + NavContext nav = getNavContext(); + String repoName = nav.getRepoName(); + String repoID = nav.getRepoID(); + + showPasswordDialog(repoName, repoID, new TaskDialog.TaskDialogListener() { + @Override + public void onTaskSuccess() { + refreshDir(); + } + }, null); + } + + public void showPasswordDialog(String repoName, String repoID, + TaskDialog.TaskDialogListener listener, String password) { + PasswordDialog passwordDialog = new PasswordDialog(); + passwordDialog.setRepo(repoName, repoID, mAccount); + if (password != null) { + passwordDialog.setPassword(password); + } + passwordDialog.setTaskDialogLisenter(listener); + passwordDialog.show(getSupportFragmentManager(), PASSWORD_DIALOG_FRAGMENT_TAG); + } + + private void chooseDir() { + chooseDir(false); + } + + private void chooseDir(boolean forceRefresh) { + mStep = STEP_CHOOSE_DIR; + mUpLayout.setVisibility(View.VISIBLE); + mEmptyText.setText(R.string.dir_empty); + + setListAdapter(getDirentsAdapter()); + refreshDir(forceRefresh); + } + + private void chooseAccount() { + chooseAccount(true); + } + + /** + * List all accounts + */ + private void chooseAccount(boolean forwardIfOnlyOneAccount) { + mStep = STEP_CHOOSE_ACCOUNT; + mUpLayout.setVisibility(View.INVISIBLE); + mEmptyText.setText(R.string.no_account); + mCurrentDir = getString(R.string.settings_cuc_remote_lib_account); + setCurrentDirText(mCurrentDir); + + mLoadAccountsTask = new LoadAccountsTask(getAccountManager(), forwardIfOnlyOneAccount); + + ConcurrentAsyncTask.execute(mLoadAccountsTask); + setListAdapter(getAccountAdapter()); + } + + /** + * List all repos + */ + private void chooseRepo() { + chooseRepo(false); + } + + private void chooseRepo(boolean forceRefresh) { + mStep = STEP_CHOOSE_REPO; + mUpLayout.setVisibility(View.VISIBLE); + mCurrentDir = mAccount.getDisplayName(); + setCurrentDirText(mCurrentDir); + + setListAdapter(getReposAdapter()); + + getNavContext().setRepoID(null); + + if (!Utils.isNetworkOn() || !forceRefresh) { + List repos = getDataManager().getReposFromCache(); + if (repos != null) { + updateAdapterWithRepos(repos); + return; + } + } + + mLoadReposTask = new LoadReposTask(getDataManager()); + ConcurrentAsyncTask.execute(mLoadReposTask); + } + + private void refreshDir() { + refreshDir(false); + } + + private void refreshDir(boolean forceRefresh) { + String repoID = getNavContext().getRepoID(); + String dirPath = getNavContext().getDirPath(); + + if (!Utils.isNetworkOn() || !forceRefresh) { + List dirents = getDataManager().getCachedDirents( + getNavContext().getRepoID(), getNavContext().getDirPath()); + if (dirents != null) { + updateAdapterWithDirents(dirents); + return; + } + } + + mLoadDirTask = new LoadDirTask(repoID, dirPath, getDataManager()); + ConcurrentAsyncTask.execute(mLoadDirTask); + } + + private void updateAdapterWithDirents(List dirents) { + getDirentsAdapter().setDirents(dirents); + showListOrEmptyText(dirents.size()); + } + + private void updateAdapterWithRepos(List repos) { + // remove encrypted repos in order to "hide" them in selection list + List filteredRepos = Lists.newArrayList(); + for (SeafRepo repo : repos) { + if (!repo.encrypted) + filteredRepos.add(repo); + } + getReposAdapter().setRepos(filteredRepos); + showListOrEmptyText(filteredRepos.size()); + } + + private CloudLibraryAccountAdapter getAccountAdapter() { + if (mAccountAdapter == null) { + mAccountAdapter = new CloudLibraryAccountAdapter(this); + } + + return mAccountAdapter; + } + + private CloudLibraryAdapter getReposAdapter() { + if (mReposAdapter == null) { + mReposAdapter = new CloudLibraryAdapter(onlyShowWritableRepos, encryptedRepoId); + } + + return mReposAdapter; + } + + private DirentsAdapter getDirentsAdapter() { + if (mDirentsAdapter == null) { + mDirentsAdapter = new DirentsAdapter(); + } + + return mDirentsAdapter; + } + + private void showListOrEmptyText(int listSize) { + if (listSize == 0) { + mFoldersListView.setVisibility(View.GONE); + mEmptyText.setVisibility(View.VISIBLE); + } else { + mFoldersListView.setVisibility(View.VISIBLE); + mEmptyText.setVisibility(View.GONE); + } + } + + private void setListAdapter(BaseAdapter adapter) { + mFoldersListView.setAdapter(adapter); + } + + private DataManager getDataManager() { + if (mDataManager == null) { + mDataManager = new DataManager(mAccount); + } + + return mDataManager; + } + + private void setAccount(Account account) { + mAccount = account; + mDataManager = new DataManager(account); + } + + private AccountManager getAccountManager() { + if (mAccountManager == null) { + mAccountManager = new AccountManager(this); + } + + return mAccountManager; + } + + private NavContext getNavContext() { + if (mNavContext == null) { + mNavContext = new NavContext(); + } + + return mNavContext; + } + + /** + * Sets the current directory's text. + */ + private void setCurrentDirText(String text) { + mCurrentFolderText.setText(text); + } + + @Override + public void onDestroy() { + super.onDestroy(); + finish(); + } + + @Override + public void onPause() { + super.onPause(); + finish(); + } + +// @Override +// public void onDestroyView() { +// super.onDestroyView(); +// +// if (isRemoving()) { +// mCursor.close(); +// mCursor = null; +// } +// +// } + + public RemoteLibrarySelectionActivity getActivity(){ + return this; + } + + private class LoadAccountsTask extends AsyncTask { + private List accounts; + private Exception err; + private AccountManager accountManager; + private boolean forwardIfOnlyOneAccount; + + public LoadAccountsTask(AccountManager accountManager, boolean forwardIfOnlyOneAccount) { + this.accountManager = accountManager; + this.forwardIfOnlyOneAccount = forwardIfOnlyOneAccount; + } + + @Override + protected void onPreExecute() { + showLoading(true); + } + + @Override + protected Void doInBackground(Void... params) { + try { + accounts = accountManager.getAccountList(); + } catch (Exception e) { + err = e; + } + + return null; + } + + @SuppressLint("LongLogTag") + @Override + protected void onPostExecute(Void v) { + showLoading(false); + if (err != null || accounts == null) { + setErrorMessage(R.string.load_accounts_fail); + if (err != null) { + Log.d(DEBUG_TAG, "failed to load accounts: " + err.getMessage()); + } + return; + } + + if (accounts.size() == 1 && forwardIfOnlyOneAccount) { + // Only 1 account. Go to next step. + setAccount(accounts.get(0)); + chooseRepo(); + return; + } + + CloudLibraryAccountAdapter adapter = getAccountAdapter(); + adapter.clear(); + for (Account account: accounts) { + adapter.add(account); + } + adapter.notifyDataSetChanged(); + showListOrEmptyText(accounts.size()); + } + } + + private class LoadReposTask extends AsyncTask { + private List repos; + private SeafException err; + private DataManager dataManager; + + public LoadReposTask(DataManager dataManager) { + this.dataManager = dataManager; + } + + @Override + protected Void doInBackground(Void... params) { + try { + repos = dataManager.getReposFromServer(); + } catch (SeafException e) { + err = e; + } + + return null; + } + + @Override + protected void onPreExecute() { + showLoading(true); + } + + @SuppressLint("LongLogTag") + @Override + protected void onPostExecute(Void v) { + if (mStep != STEP_CHOOSE_REPO) { + return; + } + + showLoading(false); + if (err != null || repos == null) { + setErrorMessage(R.string.load_libraries_fail); + Log.d(DEBUG_TAG, "failed to load repos: " + (err != null ? err.getMessage() : " no error present")); + return; + } + + updateAdapterWithRepos(repos); + } + } + + private class LoadDirTask extends AsyncTask { + private String repoID, dirPath; + private SeafException err; + private DataManager dataManager; + private List dirents; + + public LoadDirTask(String repoID, String dirPath, DataManager dataManager) { + this.repoID = repoID; + this.dirPath = dirPath; + this.dataManager = dataManager; + } + + @Override + protected void onPreExecute() { + showLoading(true); + } + + @Override + protected Void doInBackground(Void... params) { + try { + dirents = dataManager.getDirentsFromServer(repoID, dirPath); + } catch (SeafException e) { + err = e; + } + + return null; + } + + @SuppressLint("LongLogTag") + @Override + protected void onPostExecute(Void v) { + if (mStep != STEP_CHOOSE_DIR) { + return; + } + + getDirentsAdapter().clearDirents(); + showLoading(false); + if (err != null) { + int retCode = err.getCode(); + if (retCode == SeafConnection.HTTP_STATUS_REPO_PASSWORD_REQUIRED) { + showPasswordDialog(); + } else if (retCode == HttpURLConnection.HTTP_NOT_FOUND) { + final String message = String.format(getString(R.string.op_exception_folder_deleted), dirPath); + getActivity().showShortToast(getActivity(), message); + } else { + Log.d(DEBUG_TAG, "failed to load dirents: " + err.getMessage()); + err.printStackTrace(); + setErrorMessage(R.string.load_dir_fail); + } + return; + } + + if (dirents == null) { + Log.d(DEBUG_TAG, "failed to load dirents: no error present"); + setErrorMessage(R.string.load_dir_fail); + return; + } + + updateAdapterWithDirents(dirents); + } + } + + /** + * asynchronously load avatars + * + * @param avatarSize set a avatar size in one of 24*24, 32*32, 48*48, 64*64, 72*72, 96*96 + */ + public void loadAvatarUrls(int avatarSize) { + List avatars; + + if (!Utils.isNetworkOn() || !avatarManager.isNeedToLoadNewAvatars()) { + // Toast.makeText(AccountsActivity.this, getString(R.string.network_down), Toast.LENGTH_SHORT).show(); + + // use cached avatars + avatars = avatarManager.getAvatarList(); + + if (avatars == null) { + return; + } + + // set avatars url to adapter + mAccountAdapter.setAvatars((ArrayList) avatars); + + // notify adapter data changed + mAccountAdapter.notifyDataSetChanged(); + + return; + } + + LoadAvatarUrlsTask task = new LoadAvatarUrlsTask(avatarSize); + + ConcurrentAsyncTask.execute(task); + + } + + private class LoadAvatarUrlsTask extends AsyncTask> { + + private List avatars; + private int avatarSize; + private SeafConnection httpConnection; + + public LoadAvatarUrlsTask(int avatarSize) { + this.avatarSize = avatarSize; + this.avatars = Lists.newArrayList(); + } + + @Override + protected List doInBackground(Void... params) { + // reuse cached avatars + avatars = avatarManager.getAvatarList(); + + // contains accounts who don`t have avatars yet + List acts = avatarManager.getAccountsWithoutAvatars(); + + // contains new avatars in order to persist them to database + List newAvatars = new ArrayList(acts.size()); + + // load avatars from server + for (Account account : acts) { + httpConnection = new SeafConnection(account); + + String avatarRawData = null; + try { + avatarRawData = httpConnection.getAvatar(account.getEmail(), avatarSize); + } catch (SeafException e) { + e.printStackTrace(); + return avatars; + } + + Avatar avatar = avatarManager.parseAvatar(avatarRawData); + if (avatar == null) + continue; + + avatar.setSignature(account.getSignature()); + + avatars.add(avatar); + + newAvatars.add(avatar); + } + + // save new added avatars to database + avatarManager.saveAvatarList(newAvatars); + + return avatars; + } + + @Override + protected void onPostExecute(List avatars) { + if (avatars == null) { + return; + } + + // set avatars url to adapter + mAccountAdapter.setAvatars((ArrayList) avatars); + + // notify adapter data changed + mAccountAdapter.notifyDataSetChanged(); + } + } + + private void setErrorMessage(int resID) { + //mContentArea.setVisibility(View.GONE); + mErrorText.setVisibility(View.VISIBLE); + mErrorText.setText(getString(resID)); + } + + private void clearError() { + mErrorText.setVisibility(View.GONE); + //mContentArea.setVisibility(View.VISIBLE); + } + + private void showLoading(boolean loading) { + clearError(); + if (loading) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + this, android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + this, android.R.anim.fade_out)); + + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.INVISIBLE); + } else { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + this, android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + this, android.R.anim.fade_in)); + + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } + } + +} + diff --git a/app/src/main/java/com/seafile/seadroid2/loopimages/ShowImageFragment.java b/app/src/main/java/com/seafile/seadroid2/loopimages/ShowImageFragment.java new file mode 100644 index 000000000..8b58775ab --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/loopimages/ShowImageFragment.java @@ -0,0 +1,262 @@ +package com.seafile.seadroid2.loopimages; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.ui.WidgetUtils; +import com.seafile.seadroid2.ui.dialog.DeleteFileDialog; +import com.seafile.seadroid2.ui.dialog.TaskDialog; +import com.seafile.seadroid2.util.ConcurrentAsyncTask; +import com.seafile.seadroid2.util.Utils; + +import java.io.File; + +/** + * A simple {@link Fragment} subclass. + * create an instance of this fragment. + */ +public class ShowImageFragment extends Fragment { + public static final String DEBUG_TAG = "GalleryActivity"; + + private ImageView mImageView; + private TextView mImageInfoTextView; + private ImageView mDeleteBtn; + private ImageView mStarBtn; + private ImageView mShareBtn; + private RelativeLayout mToolbar; + private DataManager dataMgr; + private DirInfo dirInfo; + private String fileName; + public static int taskID; + private LoopImagesWidgetConfigureActivity mActivity; + private Account mAccount; + + public static final int MAX_BITMAP_EDGE = 1024; + + /** flag to mark if the tool bar was shown */ + private boolean showToolBar = true; + private View.OnClickListener onClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.loopimages_delete_photo: + deleteFile(dirInfo, fileName); + break; + case R.id.loopimages_star_photo: + starFile(dirInfo.getRepoId(), dirInfo.getDirPath(), fileName); + break; + case R.id.loopimages_share_photo: + shareFile(dirInfo.getRepoId(), false, Utils.pathJoin(dirInfo.getDirPath(), fileName)); + break; + } + } + }; + +// @Override +// public void onSaveInstanceState(@NonNull Bundle outState) { +// super.onSaveInstanceState(outState); +// mActivity.finish(); +// } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mActivity = (LoopImagesWidgetConfigureActivity) getActivity(); + Context context = mActivity.getApplicationContext(); + View rootView = mActivity.getLayoutInflater().inflate(R.layout.loop_imaget_show_image_fragment, container, false); + + mDeleteBtn = (ImageView) rootView.findViewById(R.id.loopimages_delete_photo); + mStarBtn = (ImageView) rootView.findViewById(R.id.loopimages_star_photo); + mShareBtn = (ImageView) rootView.findViewById(R.id.loopimages_share_photo); + mToolbar = (RelativeLayout) rootView.findViewById(R.id.loopimages_tool_bar); + mDeleteBtn.setOnClickListener(onClickListener); + mStarBtn.setOnClickListener(onClickListener); + mShareBtn.setOnClickListener(onClickListener); + + mImageView = (ImageView) rootView.findViewById(R.id.loopimages_show_image_image_view); + mImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideOrShowToolBar(); + } + }); + + mImageInfoTextView = (TextView) rootView.findViewById(R.id.loopimages_image_info); + + Intent intent = mActivity.getIntent(); + dirInfo = LoopImagesWidgetConfigureActivity.getDirInfoFromString(context, intent.getStringExtra(LoopImagesWidget.DIR_INFO)); + fileName = intent.getStringExtra(LoopImagesWidget.IMAGE_NAME); + mAccount = dirInfo.getAccount(); + dataMgr = new DataManager(mAccount); + + displayPhotos(dirInfo, fileName); + + return rootView; + } + + + private void displayPhotos(DirInfo dirInfo, String fileName) { + if(dirInfo == null || fileName == null || fileName.length() <= 0){ + return; + } + // calculate thumbnail urls by cached dirents + File file = dataMgr.getLocalRepoFile(dirInfo.getRepoName(), dirInfo.getRepoId(), Utils.pathJoin(dirInfo.getDirPath(), fileName)); + if(file == null){ + return; + } + Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); + if(bitmap == null){ + return; + } + if(bitmap.getHeight() > MAX_BITMAP_EDGE || bitmap.getWidth() > MAX_BITMAP_EDGE){ + float scale = MAX_BITMAP_EDGE/(float)Math.max(bitmap.getHeight(), bitmap.getWidth()); + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + if(bitmap == null){ + return; + } + mImageView.setImageBitmap(bitmap); + mImageInfoTextView.setText(dirInfo.getRepoName()+":"+Utils.pathJoin(dirInfo.getDirPath(), fileName)); + } + + + /** + * This method will get called when tapping at the center of a photo, + * tool bar will auto hide when open the gallery, + * and will show or hide alternatively when tapping. + */ + public void hideOrShowToolBar() { + if (showToolBar) { + mToolbar.setVisibility(View.VISIBLE); + mImageInfoTextView.setVisibility(View.VISIBLE); + } else { + mToolbar.setVisibility(View.GONE); + mImageInfoTextView.setVisibility(View.GONE); + } + showToolBar = !showToolBar; + + } + public void showShortToast(final Activity activity, final String message) { + if (activity == null) + return; + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show(); + } + + public void showShortToast(final Activity activity, final int resId) { + if (activity == null) + return; + + showShortToast(activity, activity.getString(resId)); + } + + public void showLongToast(final Activity activity, final String message) { + if (activity == null) + return; + Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); + } + + public void showLongToast(final Activity activity, final int resId) { + if (activity == null) + return; + + showLongToast(activity, activity.getString(resId)); + } + + + private void deleteFile(DirInfo dirInfo, String fileName) { + String path = Utils.pathJoin(dirInfo.getDirPath(), fileName); + File file = dataMgr.getLocalRepoFile(dirInfo.getRepoName(), dirInfo.getRepoId(), path); + final DeleteFileDialog dialog = new DeleteFileDialog(); + dialog.init(dirInfo.getRepoId(), path, false, mAccount); + dialog.setTaskDialogLisenter(new TaskDialog.TaskDialogListener() { + @Override + public void onTaskSuccess() { + showShortToast(mActivity, R.string.delete_successful); + if(file != null || file.exists()){ + file.delete(); + } + mActivity.finish(); + } + }); + dialog.show(mActivity.getSupportFragmentManager(), "DialogFragment"); + } + + private void starFile(String repoId, String dir, String fileName) { + if (!Utils.isNetworkOn()) { + showShortToast(mActivity, R.string.network_down); + return; + } + + String p = Utils.pathJoin(dir, fileName); + ConcurrentAsyncTask.execute(new StarFileTask(repoId, p)); + } + + private void shareFile(String repoID, boolean isEncrypt, String path) { + if (isEncrypt) { + WidgetUtils.inputSharePassword(mActivity, repoID, path, false, mAccount); + } else { + WidgetUtils.chooseShareApp(mActivity, repoID, path, false, mAccount, null, null); + } + } + + class StarFileTask extends AsyncTask { + private String repoId; + private String path; + private SeafException err; + + public StarFileTask(String repoId, String path) { + this.repoId = repoId; + this.path = path; + } + + @Override + protected Void doInBackground(Void... params) { + + if (dataMgr == null) + return null; + + try { + dataMgr.star(repoId, path); + } catch (SeafException e) { + err = e; + } + + return null; + } + + @Override + protected void onPostExecute(Void v) { + if (err != null) { + showShortToast(mActivity, R.string.star_file_failed); + return; + } + + showShortToast(mActivity, R.string.star_file_succeed); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/monitor/FileMonitorService.java b/app/src/main/java/com/seafile/seadroid2/monitor/FileMonitorService.java index 263c3a4ee..6302a7c0e 100644 --- a/app/src/main/java/com/seafile/seadroid2/monitor/FileMonitorService.java +++ b/app/src/main/java/com/seafile/seadroid2/monitor/FileMonitorService.java @@ -61,8 +61,10 @@ public IBinder onBind(Intent intent) { public void onCreate() { Log.d(DEBUG_TAG, "onCreate"); - Intent bindIntent = new Intent(this, TransferService.class); - bindService(bindIntent, mTransferConnection, Context.BIND_AUTO_CREATE); + if(mTransferConnection != null) { + Intent bindIntent = new Intent(this, TransferService.class); + bindService(bindIntent, mTransferConnection, Context.BIND_AUTO_CREATE); + } LocalBroadcastManager.getInstance(this).registerReceiver(transferReceiver, new IntentFilter(TransferManager.BROADCAST_ACTION)); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java index 33b2f1320..a531446ac 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java @@ -1,12 +1,16 @@ package com.seafile.seadroid2.ui.activity; import android.Manifest; +import android.app.PendingIntent; +import android.app.RecoverableSecurityException; +import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -38,6 +42,7 @@ import android.view.View; import android.view.Window; import android.widget.FrameLayout; +import android.widget.RemoteViews; import com.google.common.collect.Lists; import com.seafile.seadroid2.R; @@ -57,6 +62,9 @@ import com.seafile.seadroid2.data.ServerInfo; import com.seafile.seadroid2.data.StorageManager; import com.seafile.seadroid2.fileschooser.MultiFileChooserActivity; +import com.seafile.seadroid2.loopimages.LoopImagesWidget; +import com.seafile.seadroid2.loopimages.LoopImagesWidgetConfigureActivity; +import com.seafile.seadroid2.loopimages.LoopImagesWidgetService; import com.seafile.seadroid2.monitor.FileMonitorService; import com.seafile.seadroid2.notification.DownloadNotificationProvider; import com.seafile.seadroid2.notification.UploadNotificationProvider; @@ -396,6 +404,7 @@ public void onPageScrollStateChanged(int state) { requestReadExternalStoragePermission(); Utils.startCameraSyncJob(this); syncCamera(); + startLoopImagesWidget(); } public FrameLayout getContainer() { @@ -2439,6 +2448,22 @@ private void syncCamera() { } } + private void startLoopImagesWidget(){ + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext()); + int appWidgetIds[] = appWidgetManager.getAppWidgetIds(new ComponentName(getApplicationContext(), LoopImagesWidget.class)); + if(appWidgetIds.length > 0) { + LoopImagesWidgetConfigureActivity.init(getApplicationContext()); + Intent intent = new Intent(getApplicationContext(), LoopImagesWidgetService.class); + intent.putExtra(LoopImagesWidgetService.DELAY_UPDATE_ALL_SIGNAL, true); + getApplicationContext().startService(intent); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// getApplicationContext().startForegroundService(intent); +// } else { +// getApplicationContext().startService(intent); +// } + } + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(CheckUploadServiceEvent result) { if (!Utils.isServiceRunning(BrowserActivity.this, "com.seafile.seadroid2.cameraupload.MediaObserverService")) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/fragment/SettingsFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/fragment/SettingsFragment.java index 3bfe7cbc2..b73f2a4b7 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/fragment/SettingsFragment.java @@ -1,6 +1,7 @@ package com.seafile.seadroid2.ui.fragment; import android.app.Activity; +import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -28,6 +29,7 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.AccountInfo; import com.seafile.seadroid2.account.AccountManager; +import com.seafile.seadroid2.cameraupload.CameraSyncService; import com.seafile.seadroid2.cameraupload.CameraUploadConfigActivity; import com.seafile.seadroid2.cameraupload.CameraUploadManager; import com.seafile.seadroid2.cameraupload.GalleryBucketUtils; @@ -312,6 +314,22 @@ public boolean onPreferenceClick(Preference preference) { cUploadRepoState = findPreference(SettingsManager.CAMERA_UPLOAD_STATE); cUploadRepoState.setSummary(Utils.getUploadStateShow(getActivity())); + cUploadRepoState.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + CameraUploadManager cameraUploadManager = new CameraUploadManager(getContext()); + Account account = cameraUploadManager.getCameraAccount(); + if(account != null) { + Bundle settingsBundle = new Bundle(); + settingsBundle.putBoolean( + ContentResolver.SYNC_EXTRAS_MANUAL, true); + settingsBundle.putBoolean( + ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + ContentResolver.requestSync(account.getAndroidAccount(), CameraUploadManager.AUTHORITY, settingsBundle); + } + return true; + } + }); // Contacts Upload // cContactsCategory = (PreferenceCategory) findPreference(SettingsManager.CONTACTS_UPLOAD_CATEGORY_KEY); diff --git a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/app/src/main/res/drawable-nodpi/example_appwidget_preview.png new file mode 100644 index 000000000..894b069a4 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/example_appwidget_preview.png differ diff --git a/app/src/main/res/drawable-nodpi/rem.png b/app/src/main/res/drawable-nodpi/rem.png new file mode 100644 index 000000000..9273cadd4 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/rem.png differ diff --git a/app/src/main/res/drawable-v21/app_widget_background.xml b/app/src/main/res/drawable-v21/app_widget_background.xml new file mode 100644 index 000000000..785445c66 --- /dev/null +++ b/app/src/main/res/drawable-v21/app_widget_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml b/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml new file mode 100644 index 000000000..007e2872f --- /dev/null +++ b/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxxhdpi/rem.png b/app/src/main/res/drawable-xxxhdpi/rem.png new file mode 100644 index 000000000..9273cadd4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/rem.png differ diff --git a/app/src/main/res/layout/cuc_activity_layout.xml b/app/src/main/res/layout/cuc_activity_layout.xml index 7bafb8d6d..0a0cb2e76 100644 --- a/app/src/main/res/layout/cuc_activity_layout.xml +++ b/app/src/main/res/layout/cuc_activity_layout.xml @@ -6,10 +6,10 @@ android:orientation="vertical" > + android:id="@+id/cuc_pager" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_above="@+id/cuc_indicator" /> + + android:id="@+id/cuc_multi_selection_error_msg" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:textSize="18sp" + android:visibility="gone" /> + + + + + + + + + + + + + + + + +