diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..8ef35a5a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml new file mode 100644 index 00000000..a8fcc84d --- /dev/null +++ b/.idea/markdown-navigator-enh.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 987f5ef5..8541e25e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index cbca316b..01c42910 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,11 +2,8 @@ - - + - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7f..35eb1ddf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/java/org/piwigo/data/model/ImageVariant.java b/app/src/main/java/org/piwigo/data/model/ImageVariant.java index fa21e570..e6e45d3e 100644 --- a/app/src/main/java/org/piwigo/data/model/ImageVariant.java +++ b/app/src/main/java/org/piwigo/data/model/ImageVariant.java @@ -26,6 +26,7 @@ import androidx.room.Relation; import java.io.Serializable; +import java.util.Objects; import static androidx.room.ForeignKey.CASCADE; @@ -61,5 +62,24 @@ public ImageVariant(int imageId, int width, int height, String storageLocation, public String storageLocation; public String lastModified; public String url; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageVariant that = (ImageVariant) o; + return id == that.id && + imageId == that.imageId && + height == that.height && + width == that.width && + Objects.equals(storageLocation, that.storageLocation) && + Objects.equals(lastModified, that.lastModified) && + Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(id, imageId, height, width, storageLocation, lastModified, url); + } } diff --git a/app/src/main/java/org/piwigo/data/model/PositionedItem.java b/app/src/main/java/org/piwigo/data/model/PositionedItem.java index af88c1a7..05d28015 100644 --- a/app/src/main/java/org/piwigo/data/model/PositionedItem.java +++ b/app/src/main/java/org/piwigo/data/model/PositionedItem.java @@ -19,6 +19,7 @@ package org.piwigo.data.model; import java.io.Serializable; +import java.util.Objects; /** * represents an item (photo, album, video, ...) in the gallery, at a defined position in the current result @@ -45,4 +46,19 @@ public T getItem() { public boolean isUpdateNeeded(){ return updateNeeded; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PositionedItem that = (PositionedItem) o; + return position == that.position && + updateNeeded == that.updateNeeded && + Objects.equals(item, that.item); + } + + @Override + public int hashCode() { + return Objects.hash(position, item, updateNeeded); + } } diff --git a/app/src/main/java/org/piwigo/data/model/VariantWithImage.java b/app/src/main/java/org/piwigo/data/model/VariantWithImage.java index 868943d4..5d829b9e 100644 --- a/app/src/main/java/org/piwigo/data/model/VariantWithImage.java +++ b/app/src/main/java/org/piwigo/data/model/VariantWithImage.java @@ -22,6 +22,7 @@ import androidx.room.Relation; import java.io.Serializable; +import java.util.Objects; public class VariantWithImage implements Serializable { @Embedded @@ -31,4 +32,18 @@ public class VariantWithImage implements Serializable { entityColumn = "id" ) public Image image; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VariantWithImage that = (VariantWithImage) o; + return Objects.equals(variant, that.variant) && + Objects.equals(image, that.image); + } + + @Override + public int hashCode() { + return Objects.hash(variant, image); + } } diff --git a/app/src/main/java/org/piwigo/data/repository/CategoriesRepository.java b/app/src/main/java/org/piwigo/data/repository/CategoriesRepository.java index 0e370462..1c211096 100644 --- a/app/src/main/java/org/piwigo/data/repository/CategoriesRepository.java +++ b/app/src/main/java/org/piwigo/data/repository/CategoriesRepository.java @@ -80,52 +80,64 @@ public Observable> getCategories(@Nullable Integer cate db = mCache; // this will keep the database if the account is switched. As the old DB will be closed this thread will be reporting an exception but we accept that for now } -// Flowable> remotes = mRestCategoryRepo.getCategories( -// categoryId, -// mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) -// .subscribeOn(ioScheduler) -// .observeOn(ioScheduler) -// .toFlowable(BackpressureStrategy.BUFFER) -// -// .zipWith(Flowable.range(0, Integer.MAX_VALUE), (restCat, counter) -> { -// -// Category c = new Category(); -// c.name = restCat.name; -// c.id = restCat.id; -// if (restCat.idUppercat != 0) { -// c.parentCatId = restCat.idUppercat; -// } -// c.nbImages = restCat.nbImages; -// c.thumbnailUrl = restCat.thumbnailUrl; -// c.globalRank = restCat.globalRank; -// c.comment = restCat.comment; -// c.nbCategories = restCat.nbCategories; -// c.representativePictureId = restCat.representativePictureId; -// c.totalNbImages = restCat.totalNbImages; -//// db.categoryDao().upsert(c); -// -// return new PositionedItem(counter, c, true); -// }) -// // TODO: delete categories in database after they have been deleted on the server -// // TODO: #90 generalize sorting -// .sorted((categoryItem1, categoryItem2) -> NaturalOrderComparator.compare(categoryItem1.getItem().globalRank, categoryItem2.getItem().globalRank)); -// -// if(db == null){ -// return remotes.toObservable(); -// }else { + Flowable remoteIDs = mRestCategoryRepo.getCategories( + categoryId, + mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .toFlowable(BackpressureStrategy.BUFFER) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (restCat, counter) -> restCat.id); + + Flowable> remotes = mRestCategoryRepo.getCategories( + categoryId, + mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .toFlowable(BackpressureStrategy.BUFFER) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (restCat, counter) -> { + + Category c = new Category(); + c.name = restCat.name; + c.id = restCat.id; + if (restCat.idUppercat != 0) { + c.parentCatId = restCat.idUppercat; + } + c.nbImages = restCat.nbImages; + c.thumbnailUrl = restCat.thumbnailUrl; + c.globalRank = restCat.globalRank; + c.comment = restCat.comment; + c.nbCategories = restCat.nbCategories; + c.representativePictureId = restCat.representativePictureId; + c.totalNbImages = restCat.totalNbImages; + db.categoryDao().upsert(c); + + return new PositionedItem(counter, c, true); + }) + // TODO: #90 generalize sorting + .sorted((categoryItem1, categoryItem2) -> NaturalOrderComparator.compare(categoryItem1.getItem().globalRank, categoryItem2.getItem().globalRank)); + +// remotes.subscribe(); + if(db == null){ + return remotes.toObservable(); + }else { return db.categoryDao().getCategoriesIn(categoryId) - .subscribeOn(ioScheduler) - .observeOn(ioScheduler) - .flattenAsFlowable(s -> s) - .zipWith(Flowable.range(0, Integer.MAX_VALUE), - (item, counter) -> { - Log.d("m_cache_sync","Read "+item.name); - return new PositionedItem(counter, item, true); + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .flattenAsFlowable(s -> s) +/* .filter(category -> { + Log.d("m_cache_sync_cat","Read "+category.name); + if(!remoteIDs.contains(category.id).blockingGet()) { + Log.d("m_cache_sync_cat","Deleted "+category.name); + return false; } - ) -// .concatWith(remotes) - .toObservable(); -// } + return true; + })*/ + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (item, counter) -> { + return new PositionedItem(counter, item, true); + }) + .concatWith(remotes) + .toObservable(); + } } /** * Called when the account is changed. @@ -139,56 +151,35 @@ public void onChanged(Account account) { } } - public void updateCategories(Integer categoryId) { - Log.d("CategoriesRepository", "getCategories"); + public void updateCategoryCache(@Nullable Integer categoryId) { CacheDatabase db; synchronized (dbAccountLock) { db = mCache; // this will keep the database if the account is switched. As the old DB will be closed this thread will be reporting an exception but we accept that for now } - Flowable restCategories = mRestCategoryRepo.getCategories( + Flowable remoteIDs = mRestCategoryRepo.getCategories( categoryId, mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) .subscribeOn(ioScheduler) .observeOn(ioScheduler) .toFlowable(BackpressureStrategy.BUFFER) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (restCat, counter) -> restCat.id); - .zipWith(Flowable.range(0, Integer.MAX_VALUE), (restCat, counter) -> { - Log.d("m_cache_sync","Found "+restCat.name); - Category c = new Category(); - c.name = restCat.name; - c.id = restCat.id; - if (restCat.idUppercat != 0) { - c.parentCatId = restCat.idUppercat; - } - c.nbImages = restCat.nbImages; - c.thumbnailUrl = restCat.thumbnailUrl; - c.globalRank = restCat.globalRank; - c.comment = restCat.comment; - c.nbCategories = restCat.nbCategories; - c.representativePictureId = restCat.representativePictureId; - c.totalNbImages = restCat.totalNbImages; - db.categoryDao().upsert(c); - - return c; - }); - restCategories.subscribe(); - - Flowable dbCategories = db.categoryDao().getCategoriesIn(categoryId) - .observeOn(ioScheduler) - .subscribeOn(ioScheduler) - .flattenAsFlowable(s -> s) - .zipWith(Flowable.range(0, Integer.MAX_VALUE), - (item, counter) -> { - if(!restCategories.contains(item).blockingGet()) { - Log.d("m_cache_sync","Deleted "+item.name); - db.imageCategoryMapDao().deleteFromCategory(item.id); - db.categoryDao().delete(item); - } - return item; + if(db != null) { + db.categoryDao().getCategoriesIn(categoryId) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .flattenAsFlowable(s -> s) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (item, counter) -> { + if(!remoteIDs.contains(item.id).blockingGet()) { + Log.d("m_cache_sync_cat","Deleted "+item.name); + db.imageCategoryMapDao().deleteFromCategory(item.id); + db.categoryDao().delete(item); } - ); - - dbCategories.subscribe(); + return new PositionedItem(counter, item, true); + }) + .toObservable() + .subscribe(); + } } } diff --git a/app/src/main/java/org/piwigo/data/repository/ImageRepository.java b/app/src/main/java/org/piwigo/data/repository/ImageRepository.java index 8fbe5212..c9d8b635 100644 --- a/app/src/main/java/org/piwigo/data/repository/ImageRepository.java +++ b/app/src/main/java/org/piwigo/data/repository/ImageRepository.java @@ -105,168 +105,136 @@ public Observable> getImages(@Nullable Integer synchronized (dbAccountLock) { db = mCache; // this will keep the database if the account is switched. As the old DB will be closed this thread will be reporting an exception but we accept that for now } -// Single> variants = db.variantDao().variantsInCategory(categoryId).subscribeOn(ioScheduler); -// AtomicReference>> variantList = new AtomicReference<>(); -// -// Single> folder = db.categoryDao().getCategoryPath(categoryId); -// AtomicReference folderStr = new AtomicReference<>(null); -// -// Flowable> remotes = mRestImageRepo.getImages(categoryId) -// .subscribeOn(ioScheduler) -// .observeOn(ioScheduler) -// .toFlowable(BackpressureStrategy.BUFFER) -// .zipWith(Flowable.range(0, Integer.MAX_VALUE), (info, counter) -> { -// Derivative d; -// switch (mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) { -// case "thumb": -// d = info.derivatives.thumb; -// break; -// case "small": -// d = info.derivatives.small; -// break; -// case "xsmall": -// d = info.derivatives.xsmall; -// break; -// case "medium": -// d = info.derivatives.medium; -// break; -// case "large": -// d = info.derivatives.large; -// break; -// case "xlarge": -// d = info.derivatives.xlarge; -// break; -// case "xxlarge": -// d = info.derivatives.xxlarge; -// break; -// case "square": -// default: -// d = info.derivatives.square; -// } -// -// Image i = new Image(info.elementUrl); -// i.name = info.name; -// i.file = info.file; -// i.id = info.id; -// i.author = info.author; -// i.description = info.comment; -// i.height = info.height; -// i.width = info.width; -// i.creationDate = info.dateCreation; -// i.availableDate = info.dateAvailable; -// db.imageDao().upsert(i); -// List join = new ArrayList<>(info.categories.size()); -// for (ImageInfo.CategoryID c : info.categories) { -// join.add(new CacheDBInternals.ImageCategoryMap(c.id, i.id)); -// } -// synchronized (variantList) { -// if (variantList.get() == null) { -// variantList.set(new HashMap<>()); -// List ab = variants.blockingGet(); -// for (ImageVariantDao.VariantInfo item : ab) { -// List varsForImage = variantList.get().get(item.imageId); -// if (varsForImage == null) { -// varsForImage = new ArrayList<>(5); -// variantList.get().put(item.imageId, varsForImage); -// } -// varsForImage.add(item); -// } -// } -// } -// -// db.imageCategoryMapDao().insert(join); -// synchronized (folderStr) { -// if (folderStr.get() == null) { -// folderStr.set(StringUtils.join(folder.blockingGet(), File.separator)); -// } -// } -// VariantWithImage existingVariant = null; -// Map addHeaders = null; -// if (variantList.get() != null) { -// List all = variantList.get().get(i.id); -// if (all != null) { -// for (ImageVariantDao.VariantInfo inf : all) { -// if (inf.url.equals(d.url)) { -// existingVariant = db.variantDao().getVariantsWithImage(inf.imageId).get(0); -// File f = new File(Uri.parse(inf.storageLocation).getPath()); -// if (f.exists()) { -// // redownload if - for whatever reason - the cached file doesn't exist anymore -// addHeaders = new HashMap<>(); -// addHeaders.put("If-Modified-Since", all.get(0).lastModified); -// } -// break; -// } -// } -// } -// } -// -// Observable dnl = downloadURL(d.url, folderStr.get(), i.file, i.id, addHeaders, db); -///* dnl.subscribe(new DisposableObserver() { -// @Override -// public void onNext(String s) { -// /* TODO: #222 -// boolean expose = mPreferences.getBool(PreferencesRepository.KEY_PREF_EXPOSE_PHOTOS); -// Log.i("ImageRepository", "downloaded " + s); -// if (expose) { -// // add file to mediastore -// ContentResolver resolver = mContext.getContentResolver(); -// ContentValues values = new ContentValues(); -// values.put(MediaStore.Images.ImageColumns.DATA, s); -// values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, i.name); -// if (i.creationDate != null) { -// values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, i.creationDate.getTime()); -// } -// if (i.description != null) { -// values.put(MediaStore.Images.ImageColumns.DESCRIPTION, i.description); -// } -// -// resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); -// } -// * / -// } -// -// @Override -// public void onError(Throwable e) { -// Log.e("ImageRepository", "download failed", e); -// } -// -// @Override -// public void onComplete() { -// } -// }); -//*/ -// // TODO: delete images in database after they have been deleted on the server -// // TODO: move image cache files if parent album was renamed -// String location = dnl.blockingFirst(null); -// -// if (location != null) { -// VariantWithImage vwi = new VariantWithImage(); -// vwi.image = i; -// vwi.variant = new ImageVariant(i.id, i.width, i.height, location, null, d.url); -// return new PositionedItem<>(counter, vwi, true); -// } else { -// return new PositionedItem<>(counter, existingVariant, false); -// } -// -// }) -// .filter(x -> x.isUpdateNeeded()) -//// TODO remove .filter(x -> x.getPosition() >= 0) -// ; -// if(db == null){ -// return remotes.toObservable(); -// }else { - // TODO: #90 implement sorting -// return db.imageDao().getImagesInCategory(categoryId) - return db.variantDao().getVariantsWithImageInCategory(categoryId) - .subscribeOn(ioScheduler) - .observeOn(ioScheduler) - .flattenAsFlowable(s -> s) - .zipWith(Flowable.range(0, Integer.MAX_VALUE), - (item, counter) -> new PositionedItem(counter, item, true)) - -// .concatWith(remotes) - .toObservable(); -// } + Single> variants = db.variantDao().variantsInCategory(categoryId).subscribeOn(ioScheduler); + AtomicReference>> variantList = new AtomicReference<>(); + + Single> folder = db.categoryDao().getCategoryPath(categoryId); + AtomicReference folderStr = new AtomicReference<>(null); + + Flowable remoteIDs = mRestImageRepo.getImages(categoryId) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .toFlowable(BackpressureStrategy.BUFFER) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (info, counter) -> info.id); + + Flowable> remotes = mRestImageRepo.getImages(categoryId) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .toFlowable(BackpressureStrategy.BUFFER) + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (info, counter) -> { + Derivative d; + switch (mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) { + case "thumb": + d = info.derivatives.thumb; + break; + case "small": + d = info.derivatives.small; + break; + case "xsmall": + d = info.derivatives.xsmall; + break; + case "medium": + d = info.derivatives.medium; + break; + case "large": + d = info.derivatives.large; + break; + case "xlarge": + d = info.derivatives.xlarge; + break; + case "xxlarge": + d = info.derivatives.xxlarge; + break; + case "square": + default: + d = info.derivatives.square; + } + + Image i = new Image(info.elementUrl); + i.name = info.name; + i.file = info.file; + i.id = info.id; + i.author = info.author; + i.description = info.comment; + i.height = info.height; + i.width = info.width; + i.creationDate = info.dateCreation; + i.availableDate = info.dateAvailable; + db.imageDao().upsert(i); + + List join = new ArrayList<>(info.categories.size()); + for (ImageInfo.CategoryID c : info.categories) { + join.add(new CacheDBInternals.ImageCategoryMap(c.id, i.id)); + } + synchronized (variantList) { + if (variantList.get() == null) { + variantList.set(new HashMap<>()); + List ab = variants.blockingGet(); + for (ImageVariantDao.VariantInfo item : ab) { + List varsForImage = variantList.get().get(item.imageId); + if (varsForImage == null) { + varsForImage = new ArrayList<>(5); + variantList.get().put(item.imageId, varsForImage); + } + varsForImage.add(item); + } + } + } + + db.imageCategoryMapDao().insert(join); + synchronized (folderStr) { + if (folderStr.get() == null) { + folderStr.set(StringUtils.join(folder.blockingGet(), File.separator)); + } + } + VariantWithImage existingVariant = null; + Map addHeaders = null; + if (variantList.get() != null) { + List all = variantList.get().get(i.id); + if (all != null) { + for (ImageVariantDao.VariantInfo inf : all) { + if (inf.url.equals(d.url)) { + existingVariant = db.variantDao().getVariantsWithImage(inf.imageId).get(0); + File f = new File(Uri.parse(inf.storageLocation).getPath()); + if (f.exists()) { + // redownload if - for whatever reason - the cached file doesn't exist anymore + addHeaders = new HashMap<>(); + addHeaders.put("If-Modified-Since", all.get(0).lastModified); + } + break; + } + } + } + } + + Observable dnl = downloadURL(d.url, folderStr.get(), i.file, i.id, addHeaders, db); + + String location = dnl.blockingFirst(null); + + if (location != null) { + VariantWithImage vwi = new VariantWithImage(); + vwi.image = i; + vwi.variant = new ImageVariant(i.id, i.width, i.height, location, null, d.url); + return new PositionedItem(counter, vwi, true); + } else { + return new PositionedItem<>(counter, existingVariant, false); + } + }); + if(db == null) { + return remotes.toObservable(); + } + return db.variantDao().getVariantsWithImageInCategory(categoryId) + .subscribeOn(ioScheduler) + .observeOn(ioScheduler) + .flattenAsFlowable(s -> s) +/* .filter(variantWithImage -> { + Log.d("m_cache_sync_img","Read "+variantWithImage.image.name); + return !remoteIDs.contains(variantWithImage.image.id).blockingGet(); + })*/ + .zipWith(Flowable.range(0, Integer.MAX_VALUE), + (item, counter) -> new PositionedItem<>(counter, item, true)) + .concatWith(remotes) + .toObservable(); } @@ -369,143 +337,32 @@ public void onChanged(Account account) { } } - public void updateImages(Integer categoryId) { - Log.d("CategoriesRepository", "getCategories"); + public void updateVariantWithImageCache(@Nullable Integer categoryId) { CacheDatabase db; synchronized (dbAccountLock) { db = mCache; // this will keep the database if the account is switched. As the old DB will be closed this thread will be reporting an exception but we accept that for now } - Single> variants = db.variantDao().variantsInCategory(categoryId).subscribeOn(ioScheduler); - AtomicReference>> variantList = new AtomicReference<>(); - - Single> folder = db.categoryDao().getCategoryPath(categoryId); - AtomicReference folderStr = new AtomicReference<>(null); - - - Flowable restImages = mRestImageRepo.getImages(categoryId) + Flowable remoteIDs = mRestImageRepo.getImages(categoryId) .subscribeOn(ioScheduler) .observeOn(ioScheduler) .toFlowable(BackpressureStrategy.BUFFER) - .zipWith(Flowable.range(0, Integer.MAX_VALUE), (info, counter) -> { - Derivative d; - switch (mPreferences.getString(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE)) { - case "thumb": - d = info.derivatives.thumb; - break; - case "small": - d = info.derivatives.small; - break; - case "xsmall": - d = info.derivatives.xsmall; - break; - case "medium": - d = info.derivatives.medium; - break; - case "large": - d = info.derivatives.large; - break; - case "xlarge": - d = info.derivatives.xlarge; - break; - case "xxlarge": - d = info.derivatives.xxlarge; - break; - case "square": - default: - d = info.derivatives.square; - } + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (info, counter) -> info.id); - Image i = new Image(info.elementUrl); - i.name = info.name; - i.file = info.file; - i.id = info.id; - i.author = info.author; - i.description = info.comment; - i.height = info.height; - i.width = info.width; - i.creationDate = info.dateCreation; - i.availableDate = info.dateAvailable; - db.imageDao().upsert(i); - - List join = new ArrayList<>(info.categories.size()); - for (ImageInfo.CategoryID c : info.categories) { - join.add(new CacheDBInternals.ImageCategoryMap(c.id, i.id)); - } - synchronized (variantList) { - if (variantList.get() == null) { - variantList.set(new HashMap<>()); - List ab = variants.blockingGet(); - for (ImageVariantDao.VariantInfo item : ab) { - List varsForImage = variantList.get().get(item.imageId); - if (varsForImage == null) { - varsForImage = new ArrayList<>(5); - variantList.get().put(item.imageId, varsForImage); - } - varsForImage.add(item); - } - } - } - - db.imageCategoryMapDao().insert(join); - synchronized (folderStr) { - if (folderStr.get() == null) { - folderStr.set(StringUtils.join(folder.blockingGet(), File.separator)); - } - } - VariantWithImage existingVariant = null; - Map addHeaders = null; - if (variantList.get() != null) { - List all = variantList.get().get(i.id); - if (all != null) { - for (ImageVariantDao.VariantInfo inf : all) { - if (inf.url.equals(d.url)) { - existingVariant = db.variantDao().getVariantsWithImage(inf.imageId).get(0); - File f = new File(Uri.parse(inf.storageLocation).getPath()); - if (f.exists()) { - // redownload if - for whatever reason - the cached file doesn't exist anymore - addHeaders = new HashMap<>(); - addHeaders.put("If-Modified-Since", all.get(0).lastModified); - } - break; - } - } - } - } - - Observable dnl = downloadURL(d.url, folderStr.get(), i.file, i.id, addHeaders, db); - - String location = dnl.blockingFirst(null); - - if (location != null) { - VariantWithImage vwi = new VariantWithImage(); - vwi.image = i; - vwi.variant = new ImageVariant(i.id, i.width, i.height, location, null, d.url); - Log.d("m_cache_sync","New variant "+vwi.image.name); - return i; - } else { - Log.d("m_cache_sync","Existing variant "+existingVariant.image.name); - return i; - } - }); - - restImages.subscribe(); - - Flowable dbImage = db.imageDao().getImagesInCategory(categoryId) + db.variantDao().getVariantsWithImageInCategory(categoryId) .subscribeOn(ioScheduler) .observeOn(ioScheduler) .flattenAsFlowable(s -> s) - .zipWith(Flowable.range(0, Integer.MAX_VALUE), - (item, counter) -> { - if(!restImages.contains(item).blockingGet()) { - Log.d("m_cache_sync","Deleted image "+item.name); - db.imageCategoryMapDao().deleteFromImage(item.id); - db.imageDao().delete(item); - } - return item; - }); - - dbImage.subscribe(); + .zipWith(Flowable.range(0, Integer.MAX_VALUE), (item, counter) -> { + if(!remoteIDs.contains(item.image.id).blockingGet()) { + Log.d("m_cache_sync_img","Deleted image "+item.image.name); + db.imageCategoryMapDao().deleteFromImage(item.image.id); + db.imageDao().delete(item.image); + } + return new PositionedItem(counter, item, true); + }) + .toObservable() + .subscribe(); } } diff --git a/app/src/main/java/org/piwigo/internal/binding/adapter/ImageViewBindingAdapter.java b/app/src/main/java/org/piwigo/internal/binding/adapter/ImageViewBindingAdapter.java index e6d96245..03047e76 100644 --- a/app/src/main/java/org/piwigo/internal/binding/adapter/ImageViewBindingAdapter.java +++ b/app/src/main/java/org/piwigo/internal/binding/adapter/ImageViewBindingAdapter.java @@ -19,6 +19,8 @@ package org.piwigo.internal.binding.adapter; import androidx.databinding.BindingAdapter; + +import android.util.Log; import android.view.ViewGroup; import android.widget.ImageView; @@ -36,10 +38,13 @@ public ImageViewBindingAdapter(Picasso picasso) { } @BindingAdapter("android:src") public void loadImage(ImageView imageView, String url) { + if(imageView.getWidth() == 0){ + Log.d("ImageViewBindingAdapter", "size unkown"); + } picasso.load(url) // TODO: Add Downloader, which retruns the proper image for the size in the request.?? // or better go with refreshing the URL? - .resize(imageView.getWidth(),0) + .resize(Math.max(100, imageView.getWidth()),0) .centerCrop() .placeholder(R.mipmap.ic_placeholder) // TODO: .networkPolicy(NetworkPolicy.OFFLINE) diff --git a/app/src/main/java/org/piwigo/internal/binding/adapter/RecyclerViewBindingAdapter.java b/app/src/main/java/org/piwigo/internal/binding/adapter/RecyclerViewBindingAdapter.java index 8936143a..6292c550 100644 --- a/app/src/main/java/org/piwigo/internal/binding/adapter/RecyclerViewBindingAdapter.java +++ b/app/src/main/java/org/piwigo/internal/binding/adapter/RecyclerViewBindingAdapter.java @@ -19,7 +19,10 @@ package org.piwigo.internal.binding.adapter; +import android.util.Log; + import androidx.databinding.BindingAdapter; +import androidx.databinding.ObservableArrayList; import androidx.recyclerview.widget.RecyclerView; import org.piwigo.ui.shared.BindingRecyclerViewAdapter; @@ -29,7 +32,7 @@ public class RecyclerViewBindingAdapter { @BindingAdapter(value = {"items", "viewBinder"}, requireAll = false) - public static void bindRecyclerView(RecyclerView recyclerView, List items, BindingRecyclerViewAdapter.ViewBinder viewBinder) { + public static void bindRecyclerView(RecyclerView recyclerView, ObservableArrayList items, BindingRecyclerViewAdapter.ViewBinder viewBinder) { RecyclerView.Adapter adapter = recyclerView.getAdapter(); if (viewBinder != null) { diff --git a/app/src/main/java/org/piwigo/ui/main/AlbumsFragment.java b/app/src/main/java/org/piwigo/ui/main/AlbumsFragment.java index ff129de8..bc99c8bf 100644 --- a/app/src/main/java/org/piwigo/ui/main/AlbumsFragment.java +++ b/app/src/main/java/org/piwigo/ui/main/AlbumsFragment.java @@ -42,6 +42,8 @@ import androidx.databinding.DataBindingUtil; import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import dagger.android.support.AndroidSupportInjection; @@ -89,7 +91,7 @@ public void onPause() { @Subscribe public void onEvent(RefreshRequestEvent event) { - binding.getViewModel().onRefresh(binding.albumRecycler.getAdapter()); + binding.getViewModel().onRefresh(binding.dataRecycler.getAdapter()); } @Override @@ -106,11 +108,29 @@ public void onResume() { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_albums, container, false); - binding.albumRecycler.setHasFixedSize(true); - binding.albumRecycler.setLayoutManager(new GridLayoutManager(getContext(), calculateColumnCount())); - binding.photoRecycler.setHasFixedSize(true); - binding.photoRecycler.setLayoutManager(new GridLayoutManager(getContext(), - calculateColumnCount() * preferences.getInt(PreferencesRepository.KEY_PREF_PHOTOS_PER_ROW))); + binding.dataRecycler.setHasFixedSize(true); + + binding.dataRecycler.setLayoutManager(new GridLayoutManager(getContext(), calculateColumnCount())); + GridLayoutManager lm = new GridLayoutManager(getContext(), + calculateColumnCount() * preferences.getInt(PreferencesRepository.KEY_PREF_PHOTOS_PER_ROW)); + lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + AlbumsViewModel.ViewElement e = getViewModel().data.get(position); + if(e == null){ + return 1; + } + else { + int t = e.getType(); + if (t == AlbumsViewModel.ViewElement.IMAGE) + return 1; + else + return preferences.getInt(PreferencesRepository.KEY_PREF_PHOTOS_PER_ROW); + } + } + }); + binding.dataRecycler.setLayoutManager(lm); + binding.swiperefresh.setOnRefreshListener(this); return binding.getRoot(); } @@ -134,9 +154,8 @@ private int calculateColumnCount() { return (int) Math.floor(configuration.screenWidthDp / (largeScreen ? TABLET_MIN_WIDTH : PHONE_MIN_WIDTH)); } - @Override public void onRefresh() { - binding.getViewModel().onRefresh(binding.albumRecycler.getAdapter()); + binding.getViewModel().onRefresh(binding.dataRecycler.getAdapter()); } } diff --git a/app/src/main/java/org/piwigo/ui/main/AlbumsViewModel.java b/app/src/main/java/org/piwigo/ui/main/AlbumsViewModel.java index 4c952240..5a3ab715 100644 --- a/app/src/main/java/org/piwigo/ui/main/AlbumsViewModel.java +++ b/app/src/main/java/org/piwigo/ui/main/AlbumsViewModel.java @@ -55,10 +55,13 @@ public class AlbumsViewModel extends ViewModel { private boolean isLoadingImages = false; public ObservableBoolean isLoading = new ObservableBoolean(); - public ObservableArrayList images = new ObservableArrayList<>(); - public ObservableArrayList albums = new ObservableArrayList<>(); + public ObservableArrayList data = new ObservableArrayList<>(); + /* only modify while holding lock on data */ + private int nrOfAlbums = 0; + public BindingRecyclerViewAdapter.ViewBinder albumsViewBinder = new CategoryViewBinder(); public BindingRecyclerViewAdapter.ViewBinder photoViewBinder = new ImagesViewBinder(); + public BindingRecyclerViewAdapter.ViewBinder dataViewBinder = new DataViewBinder(); private final UserManager userManager; private final CategoriesRepository categoriesRepository; @@ -75,6 +78,7 @@ public class AlbumsViewModel extends ViewModel { private RecyclerView.Adapter albumsAdapter = null; private RecyclerView.Adapter imagesAdapter = null; + private RecyclerView.Adapter dataAdapter = null; AlbumsViewModel(UserManager userManager, CategoriesRepository categoriesRepository, ImageRepository imageRepository, Resources resources) { @@ -115,16 +119,15 @@ public void forcedLoadAlbums() { return; } if (userManager.isGuest(account) || userManager.sessionCookie() != null) { - - categoriesRepository.updateCategories(category); - + synchronized (data) { + nrOfAlbums = 0; + data.clear(); + } EspressoIdlingResource.moreBusy("load categories"); categoriesRepository.getCategories(category) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new CategoriesSubscriber()); - imageRepository.updateImages(category); - EspressoIdlingResource.moreBusy("load album images"); imageRepository.getImages(category) .observeOn(AndroidSchedulers.mainThread()) @@ -144,9 +147,8 @@ public void setMainViewModel(MainViewModel vm){ mMainViewModel = vm; } - public void onRefresh(RecyclerView.Adapter albumsAdapter) { - this.albumsAdapter = albumsAdapter; - albums.clear(); + public void onRefresh(RecyclerView.Adapter dataAdapter/*, RecyclerView.Adapter imagesAdapter*/) { + this.dataAdapter = dataAdapter; forcedLoadAlbums(); } @@ -155,7 +157,6 @@ private void updateLoading() { } private class CategoriesSubscriber extends DisposableObserver> { - public CategoriesSubscriber(){ super(); isLoadingCategories = true; @@ -164,9 +165,22 @@ public CategoriesSubscriber(){ @Override public void onComplete() { - if(albumsAdapter != null) { albumsAdapter.notifyDataSetChanged(); } - isLoadingCategories = false; - updateLoading(); + synchronized (data) { + /* TODO: remove deleted items + int i = albums.size() - nbCat - 1; + if (albumsAdapter != null) { + while (i > 0) { + Log.d("m_cache_sync", "remove album"); + data.remove(albums.size() - 1); + albums.remove(albums.size() - 1); + i--; + } + albumsAdapter.notifyDataSetChanged(); + } + */ + isLoadingCategories = false; + updateLoading(); + } EspressoIdlingResource.lessBusy("load categories", "onComplete"); } @@ -192,12 +206,23 @@ public void onError(Throwable e) { @Override public void onNext(PositionedItem category) { - - while(albums.size() <= category.getPosition()) { - albums.add(null); + synchronized (data) { + while (nrOfAlbums <= category.getPosition()) { + data.add(nrOfAlbums, null); + nrOfAlbums++; + } + if (data.get(category.getPosition()) == null){ + data.set(category.getPosition(), new ViewElement(category.getItem())); + }else{ + ViewElement ve = data.get(category.getPosition()); + if(ve == null || ve.getCategory() == null){ + Log.d("AlbumsViewModel", "data element at " + category.getPosition() + " has wrong type"); + }else + if(category.getItem().id != ve.getCategory().id) { + data.set(category.getPosition(), new ViewElement(category.getItem())); + } + } } - Log.d("m_cache","Albums size: "+albums.size()); - albums.set(category.getPosition(), category.getItem()); } } @@ -232,22 +257,41 @@ public ImageSubscriber(){ isLoadingImages = true; updateLoading(); } + @Override public void onNext(PositionedItem item) { - if(images.size() == item.getPosition()){ - images.add(item.getItem()); - }else { - while (images.size() <= item.getPosition()) { - images.add(null); + synchronized (data) { + // add empty spaces until the item position is available + while (data.size() <= nrOfAlbums + item.getPosition()) { + data.add(null); + } + if (data.get(nrOfAlbums + item.getPosition()) != null + && data.get(nrOfAlbums + item.getPosition()).getType() == ViewElement.CATEGORY){ + Log.d("AlbumsViewModel", "wrong type at index " + nrOfAlbums + item.getPosition() + " expected IMAGE found CATEGORY"); + }else if (data.get(nrOfAlbums + item.getPosition()) == null + || item.getItem().image.id != data.get(nrOfAlbums + item.getPosition()).getImage().image.id) { + data.set(nrOfAlbums + item.getPosition(), new ViewElement(item.getItem())); } - images.set(item.getPosition(), item.getItem()); } } @Override public void onComplete() { - isLoadingImages = false; - updateLoading(); + synchronized (data) { + // TODO: remove data not available anymore +/* int i = images.size() - nbImage - 1; + if (albumsAdapter != null) { + while (i > 0) { + Log.d("m_cache_sync", "removed item"); + images.remove(images.size() - 1); + data.remove(nrOfAlbums + images.size() - 1); + i--; + } + imagesAdapter.notifyDataSetChanged(); + }*/ + isLoadingImages = false; + updateLoading(); + } EspressoIdlingResource.lessBusy("load album images", "ImageSubscriber.onComplete"); } @@ -283,9 +327,96 @@ public int getLayout(int viewType) { @Override public void bind(BindingRecyclerViewAdapter.ViewHolder viewHolder, VariantWithImage image) { + Log.d("XXX", "ImagesViewBinder.bind() " + image.image.name); // TODO: make configurable to also show the photo name here - ImagesItemViewModel viewModel = new ImagesItemViewModel(image, images.indexOf(image), category); + ImagesItemViewModel viewModel; + + synchronized (data) { + int idx = 0; + while(data.get(idx).getImage() != image) idx++; + + viewModel = new ImagesItemViewModel(image, idx - nrOfAlbums, category); + } + viewHolder.getBinding().setVariable(BR.viewModel, viewModel); } } + + private class DataViewBinder implements BindingRecyclerViewAdapter.ViewBinder { + @Override + public int getViewType(ViewElement element) { + return element.type; + } + + @Override + public int getLayout(int viewType) { + if(viewType == ViewElement.IMAGE) { + return R.layout.item_images; + }else if(viewType == ViewElement.CATEGORY) { + return R.layout.item_album; + }else{ + return -1; + } + } + + @Override + public void bind(BindingRecyclerViewAdapter.ViewHolder viewHolder, ViewElement element) { + + if (element.type == ViewElement.IMAGE) { + VariantWithImage image = element.getImage(); + Log.d("XXX", "ImagesViewBinder.bind() " + image.image.name); + // TODO: make configurable to also show the photo name here + ImagesItemViewModel viewModel; + synchronized (data) { + int idx = data.indexOf(element); + viewModel = new ImagesItemViewModel(image, idx - nrOfAlbums, category); + } + viewHolder.getBinding().setVariable(BR.viewModel, viewModel); + } + if (element.type == ViewElement.CATEGORY) { + Category category = element.getCategory(); + String photos = resources.getQuantityString(R.plurals.album_photos, category.nbImages, category.nbImages); + if (category.totalNbImages > category.nbImages) { + int subPhotos = category.totalNbImages - category.nbImages; + photos += resources.getQuantityString(R.plurals.album_photos_subs, subPhotos, subPhotos); + } +// TODO: Get Image URL from local stored image instead ofr the thumbnailUrl + AlbumItemViewModel viewModel = new AlbumItemViewModel(category.thumbnailUrl, category.name, category.comment, photos, category.id); + viewHolder.getBinding().setVariable(BR.viewModel, viewModel); + } + } + } + + public class ViewElement{ + public static final int IMAGE = 0; + public static final int CATEGORY = 1; + + private int type; + private VariantWithImage image; + private Category category; + + public ViewElement(VariantWithImage image){ + this.type = IMAGE; + this.image = image; + this.category = null; + } + + public ViewElement(Category category){ + this.type = CATEGORY; + this.image = null; + this.category = category; + } + + public int getType() { + return type; + } + + public VariantWithImage getImage() { + return image; + } + + public Category getCategory() { + return category; + } + } } diff --git a/app/src/main/java/org/piwigo/ui/photoviewer/PhotoViewerDialogFragment.java b/app/src/main/java/org/piwigo/ui/photoviewer/PhotoViewerDialogFragment.java index f21711f4..7232ae15 100644 --- a/app/src/main/java/org/piwigo/ui/photoviewer/PhotoViewerDialogFragment.java +++ b/app/src/main/java/org/piwigo/ui/photoviewer/PhotoViewerDialogFragment.java @@ -56,11 +56,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa selectedPosition = getArguments().getInt("position"); pagerAdapter = new PhotoViewerPagerAdapter(getContext(), images); - - pagerAdapter.setPicassoInstance(picasso); - viewPager.setAdapter(pagerAdapter); - viewPager.setOffscreenPageLimit(3); - setCurrentItem(selectedPosition); + synchronized (pagerAdapter) { + pagerAdapter.setPicassoInstance(picasso); + viewPager.setAdapter(pagerAdapter); + viewPager.setOffscreenPageLimit(3); + } return (v); } @@ -93,15 +93,20 @@ public ImageSubscriber(){ @Override public void onNext(PositionedItem item) { - if(images.size() == item.getPosition()){ - images.add(item.getItem()); - }else { - while (images.size() <= item.getPosition()) { - images.add(null); + synchronized (pagerAdapter) { + if (images.size() == item.getPosition()) { + images.add(item.getItem()); + } else { + while (images.size() <= item.getPosition()) { + images.add(null); + } + images.set(item.getPosition(), item.getItem()); + } + pagerAdapter.notifyDataSetChanged(); + if (item.getPosition() == selectedPosition) { + setCurrentItem(selectedPosition); } - images.set(item.getPosition(), item.getItem()); } - pagerAdapter.notifyDataSetChanged(); } @Override diff --git a/app/src/main/java/org/piwigo/ui/settings/SettingsActivity.java b/app/src/main/java/org/piwigo/ui/settings/SettingsActivity.java index e5c2f17e..f2fb35f0 100644 --- a/app/src/main/java/org/piwigo/ui/settings/SettingsActivity.java +++ b/app/src/main/java/org/piwigo/ui/settings/SettingsActivity.java @@ -53,7 +53,7 @@ public static class SettingsFragment extends PreferenceFragmentCompat { private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 815; PiwigoApplication piwigo; - private ListPreference mPreferenceThumbnailSize; +// private ListPreference mPreferenceThumbnailSize; private SeekBarPreference mPreferencePhotosPerRow; private ListPreference mPreferenceDarkTheme; // TODO: #222 private SwitchPreferenceCompat mPreferenceExposePhotos; @@ -65,16 +65,16 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.settings_preferences, rootKey); mPreferencePhotosPerRow = findPreference(PreferencesRepository.KEY_PREF_PHOTOS_PER_ROW); - mPreferenceThumbnailSize = findPreference(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE); +// mPreferenceThumbnailSize = findPreference(PreferencesRepository.KEY_PREF_DOWNLOAD_SIZE); mPreferenceDarkTheme = findPreference(PreferencesRepository.KEY_PREF_COLOR_PALETTE); // TODO: #222 mPreferenceExposePhotos = findPreference(PreferencesRepository.KEY_PREF_EXPOSE_PHOTOS); mPreferencePhotosPerRow.setOnPreferenceChangeListener((preference, value) -> true); - mPreferenceThumbnailSize.setOnPreferenceChangeListener((preference, value) -> { - mPreferenceThumbnailSize.setSummary(getString(R.string.settings_download_size_summary, value.toString())); - return true; - }); +// mPreferenceThumbnailSize.setOnPreferenceChangeListener((preference, value) -> { +// mPreferenceThumbnailSize.setSummary(getString(R.string.settings_download_size_summary, value.toString())); +// return true; +// }); mPreferenceDarkTheme.setOnPreferenceChangeListener(((preference, value) -> { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { diff --git a/app/src/main/java/org/piwigo/ui/shared/BindingRecyclerViewAdapter.java b/app/src/main/java/org/piwigo/ui/shared/BindingRecyclerViewAdapter.java index 4eaa1c60..4cf8653b 100644 --- a/app/src/main/java/org/piwigo/ui/shared/BindingRecyclerViewAdapter.java +++ b/app/src/main/java/org/piwigo/ui/shared/BindingRecyclerViewAdapter.java @@ -19,10 +19,15 @@ package org.piwigo.ui.shared; +import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; +import androidx.databinding.ObservableArrayList; import androidx.databinding.ViewDataBinding; import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; + +import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -32,7 +37,7 @@ public class BindingRecyclerViewAdapter extends RecyclerView.Adapter { private final ViewBinder viewBinder; - private List items = new ArrayList(); + private ObservableArrayList items = new ObservableArrayList(); public BindingRecyclerViewAdapter(ViewBinder viewBinder) { this.viewBinder = viewBinder; @@ -55,9 +60,26 @@ public BindingRecyclerViewAdapter(ViewBinder viewBinder) { return viewBinder.getViewType(items.get(position)); } - public void update(List items) { - DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ItemDiffCB(this.items, items)); + public void update(ObservableArrayList items) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ItemDiffCB(items, this.items), false); diffResult.dispatchUpdatesTo(this); + diffResult.dispatchUpdatesTo(new ListUpdateCallback() { + @Override + public void onInserted(int position, int count) { + } + + @Override + public void onRemoved(int position, int count) { + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + } + + @Override + public void onChanged(int position, int count, @Nullable Object payload) { + } + }); // remember as old list... this.items.clear(); @@ -110,7 +132,10 @@ public int getNewListSize() { @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return oldItems.get(oldItemPosition) == newItems.get(newItemPosition); + return oldItems.get(oldItemPosition) == newItems.get(newItemPosition) + && oldItems.get(oldItemPosition) == null + || + oldItems.get(oldItemPosition).equals(newItems.get(newItemPosition)); } @Override @@ -126,5 +151,4 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldItems.get(oldItemPosition).equals(newItems.get(newItemPosition)); } } - } diff --git a/app/src/main/res/layout/fragment_albums.xml b/app/src/main/res/layout/fragment_albums.xml index 7af4ee09..088113b3 100644 --- a/app/src/main/res/layout/fragment_albums.xml +++ b/app/src/main/res/layout/fragment_albums.xml @@ -14,44 +14,17 @@ android:layout_height="match_parent" app:refreshing="@{viewModel.isLoading}"> - - - - - - - - - + app:items="@{viewModel.data}" + app:viewBinder="@{viewModel.dataViewBinder}" /> \ No newline at end of file diff --git a/app/src/main/res/xml/settings_preferences.xml b/app/src/main/res/xml/settings_preferences.xml index d061aaac..b999be44 100644 --- a/app/src/main/res/xml/settings_preferences.xml +++ b/app/src/main/res/xml/settings_preferences.xml @@ -17,7 +17,7 @@ android:title="@string/settings_photos_per_row" /> - + app:title="@string/settings_download_size" /--> - +