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" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_images_configure_activity_layout.xml b/app/src/main/res/layout/loop_images_configure_activity_layout.xml
new file mode 100644
index 000000000..51d97e22d
--- /dev/null
+++ b/app/src/main/res/layout/loop_images_configure_activity_layout.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_images_how_to_download_fragment.xml b/app/src/main/res/layout/loop_images_how_to_download_fragment.xml
new file mode 100644
index 000000000..511bb9c04
--- /dev/null
+++ b/app/src/main/res/layout/loop_images_how_to_download_fragment.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_images_widget.xml b/app/src/main/res/layout/loop_images_widget.xml
new file mode 100644
index 000000000..7c0dc1755
--- /dev/null
+++ b/app/src/main/res/layout/loop_images_widget.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_images_widget_chosen_library_fragment.xml b/app/src/main/res/layout/loop_images_widget_chosen_library_fragment.xml
new file mode 100644
index 000000000..95b89a8b2
--- /dev/null
+++ b/app/src/main/res/layout/loop_images_widget_chosen_library_fragment.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_images_widget_remote_library_configure.xml b/app/src/main/res/layout/loop_images_widget_remote_library_configure.xml
new file mode 100644
index 000000000..9e7a09f64
--- /dev/null
+++ b/app/src/main/res/layout/loop_images_widget_remote_library_configure.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loop_imaget_show_image_fragment.xml b/app/src/main/res/layout/loop_imaget_show_image_fragment.xml
new file mode 100644
index 000000000..f47ebf156
--- /dev/null
+++ b/app/src/main/res/layout/loop_imaget_show_image_fragment.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
index 655e2ada7..b6f0daa0a 100644
--- a/app/src/main/res/values-v21/styles.xml
+++ b/app/src/main/res/values-v21/styles.xml
@@ -1,7 +1,7 @@
-
+
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 4a8e70478..f34584c30 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -540,4 +540,6 @@
网络不可用
需要密码
密码太弱
+ 已选择图库
+ 下载方式
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 25a0376be..d6cd29d6a 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -1,69 +1,74 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index e79f8647d..1f971fe68 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -17,9 +17,11 @@
#f4f5f6
#fcfcfc
#888888
- #a9a9a9
+ #a9a9a9
+
#282828
- #989898
+ #989898
+
#969696
#e1e2e3
#ea8201
@@ -31,5 +33,9 @@
#FF1976D2
#FF303030
#DFA788
+ #FFE1F5FE
+ #FF81D4FA
+ #FF039BE5
+ #FF01579B
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 9c4474525..96e11478e 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,5 +1,4 @@
-
-
+ 0dp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f867514ab..79af75c2a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,394 +1,394 @@
- Seafile
- http://seafile.com
- Welcome to Seafile
- Choose an account to start
- Edit
- Delete
- Delete
- Are you sure you want to delete this library?
- Are you sure you want to delete this file or folder?
- Successfully deleted
- Delete library
- Delete file
- Recover library
- Recover file
- Move file
- Update file
- Recover folder
- Move folder
- Delete folder
- Name
- Server address
- Email or Username
- Password
- Password (at least %d characters)
- Password again
- Log In
- Open
- Download
- Download again
- Cancel
- Unknown file-type
- Can\`t find App to open this file
- Refresh
- Add Account
- Personal
- Shared
- Libraries
- Cached
- Activities
- Upload
- https
- Transfer List
- Accounts
- Settings
-
- - File
- - Photos/Videos
- - Other
-
- Choose a Seafile Server
-
- - Single Sign-On
- - Other Seafile Server
-
- cloud.seafile.com
- https://cloud.seafile.com
- No network connection
- Unknown error
- Invalid server address
- Server address can\`t be empty
- Email can\`t be empty
- Password can\`t be empty
- Password is too short
- The passwords don\`t match
- Login failed
- Email or password error
- Authentication expired, please login again
- Upload
- Uploaded
- Updated
- Download failed
- Download waiting
- Download finished
- Download cancelled
- Cancel
- Retry
- Remove
- Remove all cancelled tasks
- Remove all finished tasks
- Delete
- Rename
- Star
- Download
- Upload
- Share
- Share to WeChat
- Share link
- Share link with password
- Export
- Open with
- Delete
- Rename
- Copy
- Move
- Open as
- More
- Choose your preferred option
-
- - Export
- - Copy
- - Move
- - Star
-
-
- - Text
- - Audio
- - Video
- - Image
- - Other
-
-
- - @string/file_action_share_wechat
- - @string/file_action_share_link
- - @string/file_action_share_encrypt
-
-
- - @string/file_action_share_link
- - @string/file_action_share_encrypt
-
- Wait
- No image found
- Choose photos/videos to upload
- Select items to upload
-
- - %1$d item selected
- - %1$d items selected
-
- Please enter the name of the library
- Please enter the name of the folder
- Please enter the name of the file
- Please enter the password
- wrong password
- This library is read-only
- Error when loading the folder.\n Tap to refresh
- Error when loading libraries.\n Tap to refresh
- Error when loading activities.\n Tap to refresh
- Error when loading starred files.\n Tap to refresh
- This folder is empty
- You have no libraries yet
- No app can be found
- Add to Seafile
- OK
- Please set up an account in Seafile first
- Choose an account
- Choose a library
- Choose a folder
- Failed to load Seafile accounts
- Failed to load libraries
- Failed to load contents of this folder
- This type of share is not supported
- Edit
- Create draft
- Rename draft
- Delete draft
- Update draft
- Release draft
- Encrypt
- Download folder
- Choose your preferred option
-
- - Download folder
- - Download folder and subfolders
-
- New Library
- Add
-
- - Create file
- - Create folder
- - Upload file
- - Take a photo
-
-
- - play
- - download
-
- Create a new library
- Successfully created library %s
- Successfully created folder %s
- Successfully created file %s
- Create a new file
- Create files
- Create a new folder
- Take a photo here
- Download
- Open
- last modified
- modified
- file size
- The library has been deleted.
- The file \"%s\" has been deleted
- Export this file
- No upload tasks yet
- No download tasks yet
- Note: Server address should contain the port. For example, www.example.com:8000
- Copy link
- Share a link to this file
- Share a link to this folder
- input share password
- Generating link…
- The link is ready to be pasted.
- Rename library
- Rename file
- Rename folder
- Renamed successfully.
-
- - Files
- - Uploads
- - Accounts
- - Backup Photos
-
- No starred file yet
- Uploading started
- Photo uploading started
- Take photo successfully
- Libraries
- Activities
- Starred
- Provide the password for library \"%s\"
- Upload waiting
- Upload finished
- Upload cancelled
- Upload failed
- File already exists
- An item named \"%s\" already exist in this location.\n Do you want to replace it with the one you\`re uploading?
- Replace All
- Keep Both
- Replace
- %d days ago
- %d hours ago
- %d minutes ago
- %d seconds ago
- Just now
- Open as
- Select files to upload
- Storage was removed
- Insufficient storage space
- Error when selecting file
-
- - %1$d file selected
- - %1$d files selected
-
- Empty folder
- Choose a file
- Security check failed
- The security certificate of %s is not trusted. Do you want to continue?
- %s uses an invalid security certificate. The connection is not secure. Continue anyway?
- Untrusted Connection
- Issued To
- Fingerprints
- Period of Validity
- SHA-256 %s
- SHA-1 %s
- MD5 %s
- not available
- Serial number %s
- Valid from %s
- Valid until %s
- Yes
- No
- Remember my choice
- Copied successfully
- Moved successfully
- Internal Server Error
- Copying file
- Moving file
- Copying from %1$s \n To %2$s
- Moving from %1$s \n To %2$s
- Copying folder
- Moving folder
- Upload is started
- You can\'t copy a folder to its subfolder
- You can\'t move a folder to its subfolder
- Cancel
- Confirm
- Couldn\'t find this library. It may be deleted or encrypted
- Account
- User info
- loading…
- Space used
- Sign out
- Click to sign out
- Do you want to sign out?
- Security
- Gesture Lock
- Gesture lock on
- Gesture lock off
- Please draw Gesture Lock Pattern
- Start to draw
- Pattern cleared
- Pattern cell added
- Pattern detected
- Forgot password
- Failed 5 times, please wait 30 seconds before trying again
-
- - You can retry %1$d second later
- - You can retry %1$d seconds later
-
-
- - Incorrect pattern, you can try another %1$d time
- - Incorrect pattern, you can try another %1$d times
-
- Linking points not long enough, pelase try again
- Retry
- Next
- OK
- Please draw Gesture Lock Pattern
- Learn how to draw Gesture Lock Pattern
- Swipe across at least 4 points, try again!
- Pattern saved
- Please draw Gesture Lock pattern
- Please confirm your pattern
- Pattern didn\'t match. Try again.
- Tap OK to save your pattern
- Release finger when finished drawing
- Gesture Lock Pattern successfully saved
-
- Back
- Next
-
- Skip
-
- Contacts backup
- Enable contacts backup
- Contacts backup enabled
- Contacts backup disabled
- Please select a library for contacts backup
- Change the library for contacts backup
- Last backup time
- No backup record
- Backup
- Click to start contacts backup
-
- Camera Photo Upload
- Upload service started
- Upload service stopped
- Allow Data Plan
- Allow Videos to Upload
- Use Wi-Fi only by default
- Data plan allowed
- Upload photos only by default
- Videos included
- Change Upload Library
- Please choose a library first
- Turn on Camera Upload
- Change albums
- Auto scan photos by default
- The selected library does not exist
- Advanced Options
- Custom upload options
- Custom Upload Albums
- Pick photo albums
- Auto scan device
-
- Configuration Helper
- Thanks for using the camera upload configuration helper. \nTo help you get started with the camera upload service, you\'ll be guided through a few initial steps.
- How to upload
- Wi-Fi only
- Wi-Fi or data plan
- Choose your preferred option. \nYou can change these options later in Settings.
- What to upload
- Photos only
- Photos and videos
- Select albums
-
- - %1$d item
- - %1$d items
-
- Select Remote Library
- Choose an account
- %s was selected
- Up to Parent Folder
- Automatically guess camera albums
- Let me pick my photo albums
- Choose your preferred account and library. \nYou can change these options later in the Settings.
- Finish
- Seafile will begin uploading files from your selected folders after you confirm below.\nFeel free to close the app during this process.
- Swipe left to continue.
- Click to close the configuration helper.
- loading…
-
- Camera photos and videos
- Camera upload failed
- Server repository is missing.
- Authentication error.
- %s (renamed by Seafile)
- ABOUT
- App Version
- About the author
-
+ Seafile
+ http://seafile.com
+ Welcome to Seafile
+ Choose an account to start
+ Edit
+ Delete
+ Delete
+ Are you sure you want to delete this library?
+ Are you sure you want to delete this file or folder?
+ Successfully deleted
+ Delete library
+ Delete file
+ Recover library
+ Recover file
+ Move file
+ Update file
+ Recover folder
+ Move folder
+ Delete folder
+ Name
+ Server address
+ Email or Username
+ Password
+ Password (at least %d characters)
+ Password again
+ Log In
+ Open
+ Download
+ Download again
+ Cancel
+ Unknown file-type
+ Can\`t find App to open this file
+ Refresh
+ Add Account
+ Personal
+ Shared
+ Libraries
+ Cached
+ Activities
+ Upload
+ https
+ Transfer List
+ Accounts
+ Settings
+
+ - File
+ - Photos/Videos
+ - Other
+
+ Choose a Seafile Server
+
+ - Single Sign-On
+ - Other Seafile Server
+
+ cloud.seafile.com
+ https://cloud.seafile.com
+ No network connection
+ Unknown error
+ Invalid server address
+ Server address can\`t be empty
+ Email can\`t be empty
+ Password can\`t be empty
+ Password is too short
+ The passwords don\`t match
+ Login failed
+ Email or password error
+ Authentication expired, please login again
+ Upload
+ Uploaded
+ Updated
+ Download failed
+ Download waiting
+ Download finished
+ Download cancelled
+ Cancel
+ Retry
+ Remove
+ Remove all cancelled tasks
+ Remove all finished tasks
+ Delete
+ Rename
+ Star
+ Download
+ Upload
+ Share
+ Share to WeChat
+ Share link
+ Share link with password
+ Export
+ Open with
+ Delete
+ Rename
+ Copy
+ Move
+ Open as
+ More
+ Choose your preferred option
+
+ - Export
+ - Copy
+ - Move
+ - Star
+
+
+ - Text
+ - Audio
+ - Video
+ - Image
+ - Other
+
+
+ - @string/file_action_share_wechat
+ - @string/file_action_share_link
+ - @string/file_action_share_encrypt
+
+
+ - @string/file_action_share_link
+ - @string/file_action_share_encrypt
+
+ Wait
+ No image found
+ Choose photos/videos to upload
+ Select items to upload
+
+ - %1$d item selected
+ - %1$d items selected
+
+ Please enter the name of the library
+ Please enter the name of the folder
+ Please enter the name of the file
+ Please enter the password
+ wrong password
+ This library is read-only
+ Error when loading the folder.\n Tap to refresh
+ Error when loading libraries.\n Tap to refresh
+ Error when loading activities.\n Tap to refresh
+ Error when loading starred files.\n Tap to refresh
+ This folder is empty
+ You have no libraries yet
+ No app can be found
+ Add to Seafile
+ OK
+ Please set up an account in Seafile first
+ Choose an account
+ Choose a library
+ Choose a folder
+ Failed to load Seafile accounts
+ Failed to load libraries
+ Failed to load contents of this folder
+ This type of share is not supported
+ Edit
+ Create draft
+ Rename draft
+ Delete draft
+ Update draft
+ Release draft
+ Encrypt
+ Download folder
+ Choose your preferred option
+
+ - Download folder
+ - Download folder and subfolders
+
+ New Library
+ Add
+
+ - Create file
+ - Create folder
+ - Upload file
+ - Take a photo
+
+
+ - play
+ - download
+
+ Create a new library
+ Successfully created library %s
+ Successfully created folder %s
+ Successfully created file %s
+ Create a new file
+ Create files
+ Create a new folder
+ Take a photo here
+ Download
+ Open
+ last modified
+ modified
+ file size
+ The library has been deleted.
+ The file \"%s\" has been deleted
+ Export this file
+ No upload tasks yet
+ No download tasks yet
+ Note: Server address should contain the port. For example, www.example.com:8000
+ Copy link
+ Share a link to this file
+ Share a link to this folder
+ input share password
+ Generating link…
+ The link is ready to be pasted.
+ Rename library
+ Rename file
+ Rename folder
+ Renamed successfully.
+
+ - Files
+ - Uploads
+ - Accounts
+ - Backup Photos
+
+ No starred file yet
+ Uploading started
+ Photo uploading started
+ Take photo successfully
+ Libraries
+ Activities
+ Starred
+ Provide the password for library \"%s\"
+ Upload waiting
+ Upload finished
+ Upload cancelled
+ Upload failed
+ File already exists
+ An item named \"%s\" already exist in this location.\n Do you want to replace it with the one you\`re uploading?
+ Replace All
+ Keep Both
+ Replace
+ %d days ago
+ %d hours ago
+ %d minutes ago
+ %d seconds ago
+ Just now
+ Open as
+ Select files to upload
+ Storage was removed
+ Insufficient storage space
+ Error when selecting file
+
+ - %1$d file selected
+ - %1$d files selected
+
+ Empty folder
+ Choose a file
+ Security check failed
+ The security certificate of %s is not trusted. Do you want to continue?
+ %s uses an invalid security certificate. The connection is not secure. Continue anyway?
+ Untrusted Connection
+ Issued To
+ Fingerprints
+ Period of Validity
+ SHA-256 %s
+ SHA-1 %s
+ MD5 %s
+ not available
+ Serial number %s
+ Valid from %s
+ Valid until %s
+ Yes
+ No
+ Remember my choice
+ Copied successfully
+ Moved successfully
+ Internal Server Error
+ Copying file
+ Moving file
+ Copying from %1$s \n To %2$s
+ Moving from %1$s \n To %2$s
+ Copying folder
+ Moving folder
+ Upload is started
+ You can\'t copy a folder to its subfolder
+ You can\'t move a folder to its subfolder
+ Cancel
+ Confirm
+ Couldn\'t find this library. It may be deleted or encrypted
+ Account
+ User info
+ loading…
+ Space used
+ Sign out
+ Click to sign out
+ Do you want to sign out?
+ Security
+ Gesture Lock
+ Gesture lock on
+ Gesture lock off
+ Please draw Gesture Lock Pattern
+ Start to draw
+ Pattern cleared
+ Pattern cell added
+ Pattern detected
+ Forgot password
+ Failed 5 times, please wait 30 seconds before trying again
+
+ - You can retry %1$d second later
+ - You can retry %1$d seconds later
+
+
+ - Incorrect pattern, you can try another %1$d time
+ - Incorrect pattern, you can try another %1$d times
+
+ Linking points not long enough, pelase try again
+ Retry
+ Next
+ OK
+ Please draw Gesture Lock Pattern
+ Learn how to draw Gesture Lock Pattern
+ Swipe across at least 4 points, try again!
+ Pattern saved
+ Please draw Gesture Lock pattern
+ Please confirm your pattern
+ Pattern didn\'t match. Try again.
+ Tap OK to save your pattern
+ Release finger when finished drawing
+ Gesture Lock Pattern successfully saved
+
+ Back
+ Next
+
+ Skip
+
+ Contacts backup
+ Enable contacts backup
+ Contacts backup enabled
+ Contacts backup disabled
+ Please select a library for contacts backup
+ Change the library for contacts backup
+ Last backup time
+ No backup record
+ Backup
+ Click to start contacts backup
+
+ Camera Photo Upload
+ Upload service started
+ Upload service stopped
+ Allow Data Plan
+ Allow Videos to Upload
+ Use Wi-Fi only by default
+ Data plan allowed
+ Upload photos only by default
+ Videos included
+ Change Upload Library
+ Please choose a library first
+ Turn on Camera Upload
+ Change albums
+ Auto scan photos by default
+ The selected library does not exist
+ Advanced Options
+ Custom upload options
+ Custom Upload Albums
+ Pick photo albums
+ Auto scan device
+
+ Configuration Helper
+ Thanks for using the camera upload configuration helper. \nTo help you get started with the camera upload service, you\'ll be guided through a few initial steps.
+ How to upload
+ Wi-Fi only
+ Wi-Fi or data plan
+ Choose your preferred option. \nYou can change these options later in Settings.
+ What to upload
+ Photos only
+ Photos and videos
+ Select albums
+
+ - %1$d item
+ - %1$d items
+
+ Select Remote Library
+ Choose an account
+ %s was selected
+ Up to Parent Folder
+ Automatically guess camera albums
+ Let me pick my photo albums
+ Choose your preferred account and library. \nYou can change these options later in the Settings.
+ Finish
+ Seafile will begin uploading files from your selected folders after you confirm below.\nFeel free to close the app during this process.
+ Swipe left to continue.
+ Click to close the configuration helper.
+ loading…
+
+ Camera photos and videos
+ Camera upload failed
+ Server repository is missing.
+ Authentication error.
+ %s (renamed by Seafile)
+ ABOUT
+ App Version
+ About the author
+
Seafile Android Client
%s
@@ -402,171 +402,179 @@
Copyright ©2013-2018 Seafile Ltd.
]]>
- ADVANCED FEATURES
- CACHE STORAGE
- Cache size
- Clear cache
- Do you want to clear cache?
- Cache cleared successfully
- Cache cleared failed
- 0 KB
- Cache storage location
- Existing files will be moved to the new storage. This might take a while.
- Primary storage
- Secondary storage
- %1$s (%2$s free/%3$s total)
- %1$s (unavailable)
-
- Pull to refresh…
- Release to refresh…
- Loading…
- Last update:
- %d seconds ago
- %d minutes ago
- %d hours ago
-
- Couldn\'t open file %s.
- Can\`t open directory.
- File %s not cached.
- Couldn\'t find/open thumbnail %s.
- Couldn\'t download file %s from server.
- Can\`t open directory for reading/writing.
- Couldn\'t find Seafile account.
- Unable to upload, no path available
- Could not create directory %s
- That file already exists
- Bad request type
-
- Server is empty!
- Server needs to start with https://
- Error:
- Single Sign-On
- loading…
-
- Network connection error, please try again later!
- Cancel all tasks
- Restart
- Retry failed tasks
- Restart cancelled tasks
- Clear failed tasks
- Clear all tasks
- Clear finished tasks
- Download List
- Upload List
- All files have been downloaded
-
- - Starting to download %1$d file
- - Starting to download %1$d files
-
- Select all
- Deselect all
-
- - %d item selected
- - %d items selected
-
-
- Sort
- Sort By
-
- - Name
- - Name (Desc)
- - Last Modified Time
- - Last Modified Time (Desc)
-
-
- Search
- Searching…
- Please input keywords
- Nothing was found
- Oops, search was not supported on this server!
- Couldn\'t find this library. It may be deleted
-
- File starred successfully
- File starred failed
-
- Unstarring the file failed
- Unstar
-
- Uploading files…
- Downloading files…
- Upload completed
-
- - Uploading %1$d file (%2$d%%)
- - Uploading %1$d files (%2$d%%)
-
-
- - Downloading %1$d file (%2$d%%)
- - Downloading %1$d files (%2$d%%)
-
- Download completed
-
- Load photos error
- Encrypted library was not supported
-
- Replace existing file
- The file already exists, do you want to replace it?
- The file already exists
-
- No items were selected
-
- Navigation Drawer used to switch between major
+ ADVANCED FEATURES
+ CACHE STORAGE
+ Cache size
+ Clear cache
+ Do you want to clear cache?
+ Cache cleared successfully
+ Cache cleared failed
+ 0 KB
+ Cache storage location
+ Existing files will be moved to the new storage. This might take a while.
+ Primary storage
+ Secondary storage
+ %1$s (%2$s free/%3$s total)
+ %1$s (unavailable)
+
+ Pull to refresh…
+ Release to refresh…
+ Loading…
+ Last update:
+ %d seconds ago
+ %d minutes ago
+ %d hours ago
+
+ Couldn\'t open file %s.
+ Can\`t open directory.
+ File %s not cached.
+ Couldn\'t find/open thumbnail %s.
+ Couldn\'t download file %s from server.
+ Can\`t open directory for reading/writing.
+ Couldn\'t find Seafile account.
+ Unable to upload, no path available
+ Could not create directory %s
+ That file already exists
+ Bad request type
+
+ Server is empty!
+ Server needs to start with https://
+ Error:
+ Single Sign-On
+ loading…
+
+ Network connection error, please try again later!
+ Cancel all tasks
+ Restart
+ Retry failed tasks
+ Restart cancelled tasks
+ Clear failed tasks
+ Clear all tasks
+ Clear finished tasks
+ Download List
+ Upload List
+ All files have been downloaded
+
+ - Starting to download %1$d file
+ - Starting to download %1$d files
+
+ Select all
+ Deselect all
+
+ - %d item selected
+ - %d items selected
+
+
+ Sort
+ Sort By
+
+ - Name
+ - Name (Desc)
+ - Last Modified Time
+ - Last Modified Time (Desc)
+
+
+ Search
+ Searching…
+ Please input keywords
+ Nothing was found
+ Oops, search was not supported on this server!
+ Couldn\'t find this library. It may be deleted
+
+ File starred successfully
+ File starred failed
+
+ Unstarring the file failed
+ Unstar
+
+ Uploading files…
+ Downloading files…
+ Upload completed
+
+ - Uploading %1$d file (%2$d%%)
+ - Uploading %1$d files (%2$d%%)
+
+
+ - Downloading %1$d file (%2$d%%)
+ - Downloading %1$d files (%2$d%%)
+
+ Download completed
+
+ Load photos error
+ Encrypted library was not supported
+
+ Replace existing file
+ The file already exists, do you want to replace it?
+ The file already exists
+
+ No items were selected
+
+ Navigation Drawer used to switch between major
features of the application
-
- To process files, allow storage permission to access them.
- To process contacts, allow contacts permission to access them.
- Permission was not granted
- Modification Details
- No more activities
-
- Decrypt data locally
- Decrypt data on server side
- Encrypted library data decryption mode
-
- Clear library password
- Click to clear password
- Clear password successful
- Clear password failed
- Do you want to clear password?
- Auto clear passwords
-
- authentication token is required
- authentication token is invalid
- Two-factor Authentication token
- Authentication token can\`t be empty
- Remember that device
-
- The folder \"%s\" was deleted
- Could not find suitable app for mime type %s
- Failed to download file %s
-
- Shared with all
- Add password protection
- expiration dates
- Add auto expiration
- Please enter the expiration dates
-
- Unable to operate multiple lines
- Do you want to save and quit?
- File save successfully
- Cache cleared failed
- File save
- Description
- undo
- redo
- Link
- Error Alerts
- File Uploading Info
- File Downloading Info
+
+ To process files, allow storage permission to access them.
+ To process contacts, allow contacts permission to access them.
+ Permission was not granted
+ Modification Details
+ No more activities
+
+ Decrypt data locally
+ Decrypt data on server side
+ Encrypted library data decryption mode
+
+ Clear library password
+ Click to clear password
+ Clear password successful
+ Clear password failed
+ Do you want to clear password?
+ Auto clear passwords
+
+ authentication token is required
+ authentication token is invalid
+ Two-factor Authentication token
+ Authentication token can\`t be empty
+ Remember that device
+
+ The folder \"%s\" was deleted
+ Could not find suitable app for mime type %s
+ Failed to download file %s
+
+ Shared with all
+ Add password protection
+ expiration dates
+ Add auto expiration
+ Please enter the expiration dates
+
+ Unable to operate multiple lines
+ Do you want to save and quit?
+ File save successfully
+ Cache cleared failed
+ File save
+ Description
+ undo
+ redo
+ Link
+ Error Alerts
+ File Uploading Info
+ File Downloading Info
- Above Quota
- Upload Info
- Upload waiting
- Last sync finished at
+ Above Quota
+ Upload Info
+ Upload waiting
+ Last sync finished at
- Scanning
- Uploading
- Network unavailable
+ Scanning
+ Uploading
+ Network unavailable
- Password is required.
- Password is too weak.
+ Password is required.
+ Password is too weak.
+ EXAMPLE
+ Configure
+ Add widget
+ LoopImages widget
+ Chosen Libraries
+
+ Hello blank fragment
+ How to download
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index fbbc0452a..88811736f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,5 +1,4 @@
-
-
+
@@ -139,14 +142,13 @@
-
-
@@ -169,4 +171,14 @@
- true
- 0.4
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..9a87914fb
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/loop_images_widget_info.xml b/app/src/main/res/xml/loop_images_widget_info.xml
new file mode 100644
index 000000000..348ec1b97
--- /dev/null
+++ b/app/src/main/res/xml/loop_images_widget_info.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 1676c7316..73ec2c810 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,8 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0'
-
+ classpath 'com.android.tools.build:gradle:7.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -24,9 +23,9 @@ allprojects {
}
ext {
- compileSdkVersion = 29
+ compileSdkVersion = 30
supportLibVersion = '27.1.1' // variable that can be referenced to keep support libs consistent
- minSdkVersion = 21
+ minSdkVersion = 24
targetSdkVersion = 30
buildToolsVersion = '27.0.3'
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5f16725ca..36444c466 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip