Add tile view to media view

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2022-07-22 15:32:22 +02:00 committed by Álvaro Brey
parent ac20b55d90
commit 66d8756bec
No known key found for this signature in database
GPG key ID: 2585783189A62105
39 changed files with 1152 additions and 308 deletions

View file

@ -0,0 +1,131 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.fragment
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import androidx.test.espresso.intent.rule.IntentsTestRule
import com.nextcloud.client.TestActivity
import com.owncloud.android.AbstractIT
import com.owncloud.android.datamodel.ImageDimension
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE
import com.owncloud.android.lib.common.utils.Log_OC
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.Random
class GalleryFragmentIT : AbstractIT() {
@get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
lateinit var activity: TestActivity
val random = Random()
@Before
fun before() {
activity = testActivityRule.launchActivity(null)
createImage(1, true, 700, 300)
createImage(2, true, 500, 300)
// createImage(3, true, 300, 400)
// createImage(4, true, 600, 800)
//
// createImage(5, true, 700, 300)
// createImage(6, true, 300, 400)
createImage(7, true, 300, 400)
// for (i in 7..50) {
// createImage(i)
// }
}
@After
override fun after() {
ThumbnailsCacheManager.clearCache()
super.after()
}
@Test
fun showGallery() {
val sut = GalleryFragment()
activity.addFragment(sut)
longSleep()
}
private fun createImage(int: Int, createPreview: Boolean = true, width: Int? = null, height: Int? = null) {
val defaultSize = ThumbnailsCacheManager.getThumbnailDimension().toFloat()
val file = OCFile("/$int.png").apply {
fileId = int.toLong()
remoteId = "$int"
mimeType = "image/png"
isPreviewAvailable = true
modificationTimestamp = (1658475504 + int.toLong()) * 1000
imageDimension = ImageDimension(width?.toFloat() ?: defaultSize, height?.toFloat() ?: defaultSize)
storageManager.saveFile(this)
}
if (!createPreview) {
return
}
// create dummy thumbnail
var w: Int
var h: Int
if (width == null || height == null) {
if (random.nextBoolean()) {
// portrait
w = (random.nextInt(3) + 2) * 100 // 200-400
h = (random.nextInt(5) + 4) * 100 // 400-800
} else {
// landscape
w = (random.nextInt(5) + 4) * 100 // 400-800
h = (random.nextInt(3) + 2) * 100 // 200-400
}
} else {
w = width
h = height
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
Canvas(bitmap).apply {
drawRGB(random.nextInt(256), random.nextInt(256), random.nextInt(256))
drawCircle(w / 2f, h / 2f, Math.min(w, h) / 2f, Paint().apply { color = Color.BLACK })
}
ThumbnailsCacheManager.addBitmapToCache(PREFIX_RESIZED_IMAGE + file.remoteId, bitmap)
assertNotNull(ThumbnailsCacheManager.getBitmapFromDiskCache(PREFIX_RESIZED_IMAGE + file.remoteId))
Log_OC.d("Gallery_thumbnail", "created $int with ${bitmap.width} x ${bitmap.height}")
}
}

View file

@ -0,0 +1,37 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.utils
import com.owncloud.android.AbstractIT
import org.junit.Assert.assertEquals
import org.junit.Test
class DisplayUtilsIT : AbstractIT() {
@Test
fun testPixelToDP() {
val px = 123
val dp = DisplayUtils.convertPixelToDp(px, targetContext)
val newPx = DisplayUtils.convertDpToPixel(dp, targetContext)
assertEquals(px.toLong(), newPx.toLong())
}
}

View file

@ -276,7 +276,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
@SuppressFBWarnings("ST")
@Override
public void onCreate() {
enableStrictMode();
// enableStrictMode();
viewThemeUtils = viewThemeUtilsProvider.get();

View file

@ -509,6 +509,7 @@ public class FileDataStorageManager {
cv.put(ProviderTableMeta.FILE_LOCK_TIMEOUT, file.getLockTimeout());
cv.put(ProviderTableMeta.FILE_LOCK_TOKEN, file.getLockToken());
cv.put(ProviderTableMeta.FILE_MODIFIED, file.getModificationTimestamp());
cv.put(ProviderTableMeta.FILE_METADATA_SIZE, new Gson().toJson(file.getImageDimension()));
return cv;
}
@ -1034,6 +1035,12 @@ public class FileDataStorageManager {
ocFile.setSharees(new ArrayList<>());
}
}
String metadataSize = cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_METADATA_SIZE));
ImageDimension imageDimension = new Gson().fromJson(metadataSize, ImageDimension.class);
if (imageDimension != null) {
ocFile.setImageDimension(imageDimension);
}
}
return ocFile;

View file

@ -22,4 +22,18 @@
package com.owncloud.android.datamodel
data class GalleryItems(val date: Long, val files: List<OCFile>)
import com.owncloud.android.utils.DisplayUtils
data class GalleryItems(val date: Long, val rows: List<GalleryRow>) {
override fun toString(): String {
val month = DisplayUtils.getDateByPattern(
date,
DisplayUtils.MONTH_PATTERN
)
val year = DisplayUtils.getDateByPattern(
date,
DisplayUtils.YEAR_PATTERN
)
return "$month/$year with $rows rows"
}
}

View file

@ -0,0 +1,29 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel
data class GalleryRow(val files: List<OCFile>, val defaultHeight: Int, val defaultWidth: Int) {
fun getMaxHeight(): Float {
return files.map { it.imageDimension?.height ?: defaultHeight.toFloat() }.maxOrNull() ?: 0f
}
}

View file

@ -0,0 +1,24 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel
data class ImageDimension(var width: Float = -1f, var height: Float = -1f)

View file

@ -110,6 +110,8 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
private long lockTimeout;
@Nullable
private String lockToken;
@Nullable
private ImageDimension imageDimension;
/**
* URI to the local path of the file contents, if stored in the device; cached after first call to {@link
@ -502,6 +504,8 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
lockTimestamp = 0;
lockTimeout = 0;
lockToken = null;
imageDimension = null;
}
/**
@ -948,4 +952,13 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
public void setLockToken(@Nullable String lockToken) {
this.lockToken = lockToken;
}
public void setImageDimension(@Nullable ImageDimension imageDimension) {
this.imageDimension = imageDimension;
}
@Nullable
public ImageDimension getImageDimension() {
return imageDimension;
}
}

View file

@ -62,6 +62,7 @@ import com.owncloud.android.ui.adapter.DiskLruImageCache;
import com.owncloud.android.ui.fragment.FileFragment;
import com.owncloud.android.ui.preview.PreviewImageFragment;
import com.owncloud.android.utils.BitmapUtils;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeTypeUtil;
@ -79,6 +80,7 @@ import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@ -251,6 +253,164 @@ public final class ThumbnailsCacheManager {
return null;
}
public static class GalleryImageGenerationTaskObject {
private final OCFile file;
private final String imageKey;
public GalleryImageGenerationTaskObject(OCFile file, String imageKey) {
this.file = file;
this.imageKey = imageKey;
}
private OCFile getFile() {
return file;
}
private String getImageKey() {
return imageKey;
}
}
public static class GalleryImageGenerationTask extends AsyncTask<Object, Void, Bitmap> {
private final User user;
private final FileDataStorageManager storageManager;
private final WeakReference<ImageView> imageViewReference;
private OCFile file;
private String imageKey;
private GalleryListener listener;
private List<GalleryImageGenerationTask> asyncTasks;
private int backgroundColor;
private boolean newImage = false;
public GalleryImageGenerationTask(
ImageView imageView,
User user,
FileDataStorageManager storageManager,
List<GalleryImageGenerationTask> asyncTasks,
String imageKey,
int backgroundColor
) {
this.user = user;
this.storageManager = storageManager;
imageViewReference = new WeakReference<>(imageView);
this.asyncTasks = asyncTasks;
this.imageKey = imageKey;
this.backgroundColor = backgroundColor;
}
public void setListener(GalleryImageGenerationTask.GalleryListener listener) {
this.listener = listener;
}
public String getImageKey() {
return imageKey;
}
@Override
protected Bitmap doInBackground(Object... params) {
Bitmap thumbnail;
file = (OCFile) params[0];
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (file.getRemoteId() != null && file.isPreviewAvailable()) {
// Thumbnail in cache?
thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId()
);
if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
Float size = (float) ThumbnailsCacheManager.getThumbnailDimension();
// resized dimensions
ImageDimension imageDimension = file.getImageDimension();
if (imageDimension == null ||
imageDimension.getWidth() != size ||
imageDimension.getHeight() != size) {
file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight()));
storageManager.saveFile(file);
}
if (MimeTypeUtil.isVideo(file)) {
return ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext());
} else {
return thumbnail;
}
} else {
try {
mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(),
MainApp.getAppContext());
thumbnail = doResizedImageInBackground(file, storageManager);
newImage = true;
if (MimeTypeUtil.isVideo(file) && thumbnail != null) {
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
} catch (OutOfMemoryError oome) {
Log_OC.e(TAG, "Out of memory");
} catch (Throwable t) {
// the app should never break due to a problem with thumbnails
Log_OC.e(TAG, "Generation of gallery image for " + file + " failed", t);
}
return thumbnail;
}
}
return null;
}
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null && imageViewReference != null) {
final ImageView imageView = imageViewReference.get();
final GalleryImageGenerationTask bitmapWorkerTask = getGalleryImageGenerationTask(imageView);
if (this == bitmapWorkerTask) {
String tagId = String.valueOf(file.getFileId());
if (String.valueOf(imageView.getTag()).equals(tagId)) {
if ("image/png".equalsIgnoreCase(file.getMimeType())) {
imageView.setBackgroundColor(backgroundColor);
}
if (newImage && listener != null) {
listener.onNewGalleryImage();
}
imageView.setImageBitmap(bitmap);
imageView.invalidate();
}
}
if (listener != null) {
listener.onSuccess();
}
} else {
if (listener != null) {
listener.onError();
}
}
if (asyncTasks != null) {
asyncTasks.remove(this);
}
}
public interface GalleryListener {
void onSuccess();
void onNewGalleryImage();
void onError();
}
}
public static class ResizedImageGenerationTask extends AsyncTask<Object, Void, Bitmap> {
private final FileFragment fileFragment;
private final FileDataStorageManager storageManager;
@ -288,10 +448,10 @@ public final class ThumbnailsCacheManager {
mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(),
MainApp.getAppContext());
thumbnail = doResizedImageInBackground();
thumbnail = doResizedImageInBackground(file, storageManager);
if (MimeTypeUtil.isVideo(file) && thumbnail != null) {
thumbnail = addVideoOverlay(thumbnail);
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
} catch (OutOfMemoryError oome) {
@ -304,79 +464,6 @@ public final class ThumbnailsCacheManager {
return thumbnail;
}
private Bitmap doResizedImageInBackground() {
Bitmap thumbnail;
String imageKey = PREFIX_RESIZED_IMAGE + file.getRemoteId();
// Check disk cache in background thread
thumbnail = getBitmapFromDiskCache(imageKey);
// Not found in disk cache
if (thumbnail == null || file.isUpdateThumbnailNeeded()) {
Point p = getScreenDimension();
int pxW = p.x;
int pxH = p.y;
if (file.isDown()) {
Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getStoragePath(), pxW, pxH);
if (bitmap != null) {
// Handle PNG
if (PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
bitmap = handlePNG(bitmap, pxW, pxH);
}
thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), pxW, pxH);
file.setUpdateThumbnailNeeded(false);
storageManager.saveFile(file);
}
} else {
// Download thumbnail from server
if (mClient != null) {
GetMethod getMethod = null;
try {
String uri = mClient.getBaseUri() + "/index.php/core/preview.png?file="
+ URLEncoder.encode(file.getRemotePath())
+ "&x=" + pxW + "&y=" + pxH + "&a=1&mode=cover&forceIcon=0";
getMethod = new GetMethod(uri);
int status = mClient.executeMethod(getMethod);
if (status == HttpStatus.SC_OK) {
InputStream inputStream = getMethod.getResponseBodyAsStream();
thumbnail = BitmapFactory.decodeStream(inputStream);
} else {
mClient.exhaustResponse(getMethod.getResponseBodyAsStream());
}
// Handle PNG
if (thumbnail != null && PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
thumbnail = handlePNG(thumbnail, thumbnail.getWidth(), thumbnail.getHeight());
}
// Add thumbnail to cache
if (thumbnail != null) {
Log_OC.d(TAG, "add thumbnail to cache: " + file.getFileName());
addBitmapToCache(imageKey, thumbnail);
}
} catch (Exception e) {
Log_OC.d(TAG, e.getMessage(), e);
} finally {
if (getMethod != null) {
getMethod.releaseConnection();
}
}
}
}
}
return thumbnail;
}
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null) {
final ImageView imageView = imageViewReference.get();
@ -516,7 +603,7 @@ public final class ThumbnailsCacheManager {
thumbnail = doThumbnailFromOCFileInBackground();
if (MimeTypeUtil.isVideo((ServerFileInterface) mFile) && thumbnail != null) {
thumbnail = addVideoOverlay(thumbnail);
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
} else if (mFile instanceof File) {
thumbnail = doFileInBackground();
@ -525,7 +612,7 @@ public final class ThumbnailsCacheManager {
String mMimeType = FileStorageUtils.getMimeTypeFromName(url);
if (MimeTypeUtil.isVideo(mMimeType) && thumbnail != null) {
thumbnail = addVideoOverlay(thumbnail);
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
//} else { do nothing
}
@ -1101,22 +1188,31 @@ public final class ThumbnailsCacheManager {
return null;
}
public static Bitmap addVideoOverlay(Bitmap thumbnail) {
int playButtonWidth = (int) (thumbnail.getWidth() * 0.3);
int playButtonHeight = (int) (thumbnail.getHeight() * 0.3);
private static GalleryImageGenerationTask getGalleryImageGenerationTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncGalleryImageDrawable) {
final AsyncGalleryImageDrawable asyncDrawable = (AsyncGalleryImageDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
public static Bitmap addVideoOverlay(Bitmap thumbnail, Context context) {
// int minValue = Math.min(thumbnail.getWidth(), thumbnail.getHeight());
// int playButtonWidth = (int) (minValue * 0.2);
// int playButtonHeight = (int) (minValue * 0.2);
Drawable playButtonDrawable = ResourcesCompat.getDrawable(MainApp.getAppContext().getResources(),
R.drawable.view_play,
R.drawable.video_white,
null);
Bitmap playButton = BitmapUtils.drawableToBitmap(playButtonDrawable,
playButtonWidth,
playButtonHeight);
int px = DisplayUtils.convertDpToPixel(24f, context);
Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton,
playButtonWidth,
playButtonHeight,
true);
Bitmap playButton = BitmapUtils.drawableToBitmap(playButtonDrawable, px, px);
Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton, px, px, true);
Bitmap resultBitmap = Bitmap.createBitmap(thumbnail.getWidth(),
thumbnail.getHeight(),
@ -1124,32 +1220,31 @@ public final class ThumbnailsCacheManager {
Canvas c = new Canvas(resultBitmap);
// compute visual center of play button, according to resized image
int x1 = resizedPlayButton.getWidth();
int y1 = resizedPlayButton.getHeight() / 2;
int x2 = 0;
int y2 = resizedPlayButton.getWidth();
int x3 = 0;
int y3 = 0;
double ym = ( ((Math.pow(x3,2) - Math.pow(x1,2) + Math.pow(y3,2) - Math.pow(y1,2)) *
(x2 - x1)) - (Math.pow(x2,2) - Math.pow(x1,2) + Math.pow(y2,2) -
Math.pow(y1,2)) * (x3 - x1) ) / (2 * ( ((y3 - y1) * (x2 - x1)) -
((y2 - y1) * (x3 - x1)) ));
double xm = ( (Math.pow(x2,2) - Math.pow(x1,2)) + (Math.pow(y2,2) - Math.pow(y1,2)) -
(2*ym*(y2 - y1)) ) / (2*(x2 - x1));
// offset to top left
double ox = - xm;
// // compute visual center of play button, according to resized image
// int x1 = resizedPlayButton.getWidth();
// int y1 = resizedPlayButton.getHeight() / 2;
// int x2 = 0;
// int y2 = resizedPlayButton.getWidth();
// int x3 = 0;
// int y3 = 0;
//
// double ym = ( ((Math.pow(x3,2) - Math.pow(x1,2) + Math.pow(y3,2) - Math.pow(y1,2)) *
// (x2 - x1)) - (Math.pow(x2,2) - Math.pow(x1,2) + Math.pow(y2,2) -
// Math.pow(y1,2)) * (x3 - x1) ) / (2 * ( ((y3 - y1) * (x2 - x1)) -
// ((y2 - y1) * (x3 - x1)) ));
// double xm = ( (Math.pow(x2,2) - Math.pow(x1,2)) + (Math.pow(y2,2) - Math.pow(y1,2)) -
// (2*ym*(y2 - y1)) ) / (2*(x2 - x1));
//
// // offset to top left
// double ox = - xm;
//
c.drawBitmap(thumbnail, 0, 0, null);
Paint p = new Paint();
p.setAlpha(230);
c.drawBitmap(resizedPlayButton, (float) ((thumbnail.getWidth() / 2) + ox),
(float) ((thumbnail.getHeight() / 2) - ym), p);
c.drawBitmap(resizedPlayButton, px, px, p);
return resultBitmap;
}
@ -1183,6 +1278,19 @@ public final class ThumbnailsCacheManager {
}
}
public static class AsyncGalleryImageDrawable extends BitmapDrawable {
private final WeakReference<GalleryImageGenerationTask> bitmapWorkerTaskReference;
public AsyncGalleryImageDrawable(Resources res, Bitmap bitmap, GalleryImageGenerationTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
}
private GalleryImageGenerationTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
public static class AsyncMediaThumbnailDrawable extends BitmapDrawable {
public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) {
@ -1291,4 +1399,87 @@ public final class ThumbnailsCacheManager {
}
}
}
@VisibleForTesting
public static void clearCache() {
mThumbnailCache.clearCache();
}
private static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageManager storageManager) {
Bitmap thumbnail;
String imageKey = PREFIX_RESIZED_IMAGE + file.getRemoteId();
// Check disk cache in background thread
thumbnail = getBitmapFromDiskCache(imageKey);
// Not found in disk cache
if (thumbnail == null || file.isUpdateThumbnailNeeded()) {
Point p = getScreenDimension();
int pxW = p.x;
int pxH = p.y;
if (file.isDown()) {
Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(file.getStoragePath(), pxW, pxH);
if (bitmap != null) {
// Handle PNG
if (PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
bitmap = handlePNG(bitmap, pxW, pxH);
}
thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), pxW, pxH);
file.setUpdateThumbnailNeeded(false);
}
} else {
// Download thumbnail from server
if (mClient != null) {
GetMethod getMethod = null;
try {
String uri = mClient.getBaseUri() + "/index.php/core/preview.png?file="
+ URLEncoder.encode(file.getRemotePath())
+ "&x=" + (pxW / 2) + "&y=" + (pxH / 2) + "&a=1&mode=cover&forceIcon=0";
Log_OC.d(TAG, "generate resized image: " + file.getFileName() + " URI: " + uri);
getMethod = new GetMethod(uri);
int status = mClient.executeMethod(getMethod);
if (status == HttpStatus.SC_OK) {
InputStream inputStream = getMethod.getResponseBodyAsStream();
thumbnail = BitmapFactory.decodeStream(inputStream);
} else {
mClient.exhaustResponse(getMethod.getResponseBodyAsStream());
}
// Handle PNG
if (thumbnail != null && PNG_MIMETYPE.equalsIgnoreCase(file.getMimeType())) {
thumbnail = handlePNG(thumbnail, thumbnail.getWidth(), thumbnail.getHeight());
}
// Add thumbnail to cache
if (thumbnail != null) {
Log_OC.d(TAG, "add resized image to cache: " + file.getFileName());
addBitmapToCache(imageKey, thumbnail);
}
} catch (Exception e) {
Log_OC.d(TAG, e.getMessage(), e);
} finally {
if (getMethod != null) {
getMethod.releaseConnection();
}
}
}
}
// resized dimensions
if (thumbnail != null) {
file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight()));
storageManager.saveFile(file);
}
}
return thumbnail;
}
}

View file

@ -35,7 +35,7 @@ import java.util.List;
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 63;
public static final int DB_VERSION = 64;
private ProviderMeta() {
// No instance
@ -117,6 +117,7 @@ public class ProviderMeta {
public static final String FILE_NOTE = "note";
public static final String FILE_SHAREES = "sharees";
public static final String FILE_RICH_WORKSPACE = "rich_workspace";
public static final String FILE_METADATA_SIZE = "metadata_size";
public static final String FILE_LOCKED = "locked";
public static final String FILE_LOCK_TYPE = "lock_type";
public static final String FILE_LOCK_OWNER = "lock_owner";
@ -169,7 +170,8 @@ public class ProviderMeta {
FILE_LOCK_OWNER_EDITOR,
FILE_LOCK_TIMESTAMP,
FILE_LOCK_TIMEOUT,
FILE_LOCK_TOKEN));
FILE_LOCK_TOKEN,
FILE_METADATA_SIZE));
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
// Columns of ocshares table

View file

@ -506,6 +506,11 @@ public class RefreshFolderOperation extends RemoteOperation {
// add to updatedFile data about LOCAL STATE (not existing in server)
updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
// keep thumbnail info
if (!updatedFile.isUpdateThumbnailNeeded() && localFile != null && localFile.getImageDimension() != null) {
updatedFile.setImageDimension(localFile.getImageDimension());
}
// add to updatedFile data from local and remote file
setLocalFileDataOnUpdatedFile(remoteFile, localFile, updatedFile, mRemoteFolderChanged);

View file

@ -754,6 +754,7 @@ public class FileContentProvider extends ContentProvider {
+ ProviderTableMeta.FILE_NOTE + TEXT
+ ProviderTableMeta.FILE_SHAREES + TEXT
+ ProviderTableMeta.FILE_RICH_WORKSPACE + TEXT
+ ProviderTableMeta.FILE_METADATA_SIZE + TEXT
+ ProviderTableMeta.FILE_LOCKED + INTEGER // boolean
+ ProviderTableMeta.FILE_LOCK_TYPE + INTEGER
+ ProviderTableMeta.FILE_LOCK_OWNER + TEXT
@ -2499,6 +2500,24 @@ public class FileContentProvider extends ContentProvider {
if (!upgraded) {
Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
}
if (oldVersion < 64 && newVersion >= 64) {
Log_OC.i(SQL, "Entering in the #64 add metadata size to files");
db.beginTransaction();
try {
db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
ADD_COLUMN + ProviderTableMeta.FILE_METADATA_SIZE + " TEXT ");
upgraded = true;
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
if (!upgraded) {
Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
}
}
}
}

View file

@ -168,7 +168,7 @@ public abstract class EditorWebView extends ExternalSiteWebView {
if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
if (MimeTypeUtil.isVideo(file)) {
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail, this);
binding.thumbnail.setImageBitmap(withOverlay);
} else {
binding.thumbnail.setImageBitmap(thumbnail);

View file

@ -36,9 +36,10 @@ import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.databinding.GalleryHeaderBinding
import com.owncloud.android.databinding.GridImageBinding
import com.owncloud.android.databinding.GalleryRowBinding
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.GalleryItems
import com.owncloud.android.datamodel.GalleryRow
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.fragment.GalleryFragment
@ -47,7 +48,6 @@ import com.owncloud.android.ui.fragment.SearchType
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileSortOrder
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import me.zhanghai.android.fastscroll.PopupTextProvider
@ -61,7 +61,9 @@ class GalleryAdapter(
ocFileListFragmentInterface: OCFileListFragmentInterface,
preferences: AppPreferences,
transferServiceGetter: ComponentsGetter,
viewThemeUtils: ViewThemeUtils
viewThemeUtils: ViewThemeUtils,
var columns: Int,
val defaultThumbnailSize: Int
) : SectionedRecyclerViewAdapter<SectionedViewHolder>(), CommonOCFileListAdapterInterface, PopupTextProvider {
var files: List<GalleryItems> = mutableListOf()
private val ocFileListDelegate: OCFileListDelegate
@ -97,8 +99,12 @@ class GalleryAdapter(
)
)
} else {
GalleryItemViewHolder(
GridImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
GalleryRowHolder(
GalleryRowBinding.inflate(LayoutInflater.from(parent.context), parent, false),
defaultThumbnailSize.toFloat(),
ocFileListDelegate,
storageManager,
this
)
}
}
@ -110,19 +116,13 @@ class GalleryAdapter(
absolutePosition: Int
) {
if (holder != null) {
val itemViewHolder = holder as GalleryItemViewHolder
val ocFile = files[section].files[relativePosition]
ocFileListDelegate.bindGridViewHolder(
itemViewHolder,
ocFile,
SearchType.GALLERY_SEARCH
)
val rowHolder = holder as GalleryRowHolder
rowHolder.bind(files[section].rows[relativePosition])
}
}
override fun getItemCount(section: Int): Int {
return files[section].files.size
return files[section].rows.size
}
override fun getSectionCount(): Int {
@ -199,12 +199,20 @@ class GalleryAdapter(
files = finalSortedList
.groupBy { firstOfMonth(it.modificationTimestamp) }
.map { GalleryItems(it.key, FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(it.value)) }
.map { GalleryItems(it.key, transformToRows(it.value)) }
.sortedBy { it.date }.reversed()
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
}
private fun transformToRows(list: List<OCFile>): List<GalleryRow> {
return list
.sortedBy { it.modificationTimestamp }
.reversed()
.chunked(columns)
.map { entry -> GalleryRow(entry, defaultThumbnailSize, defaultThumbnailSize) }
}
@SuppressLint("NotifyDataSetChanged")
fun clear() {
files = emptyList()
@ -227,10 +235,14 @@ class GalleryAdapter(
}
fun getItem(position: Int): OCFile? {
val itemCoord = getRelativePosition(position)
val itemCoordinates = getRelativePosition(position)
return files
.getOrNull(itemCoord.section())?.files
?.getOrNull(itemCoord.relativePos())
.getOrNull(itemCoordinates.section())
?.rows
?.getOrNull(itemCoordinates.relativePos())
?.files
?.getOrNull(0)
}
override fun isMultiSelect(): Boolean {
@ -242,8 +254,27 @@ class GalleryAdapter(
}
override fun getItemPosition(file: OCFile): Int {
val item = files.find { it.files.contains(file) }
return getAbsolutePosition(files.indexOf(item), item?.files?.indexOf(file) ?: 0)
var item: Int? = null
var row: Int? = null
for (galleryItem in files.withIndex()) {
if (item != null) {
break
}
for (galleryRow in galleryItem.value.rows.withIndex()) {
if (galleryRow.value.files.contains(file)) {
item = galleryItem.index
row = galleryRow.index
break
}
}
}
// month, row
return if (item == null || row == null) {
getAbsolutePosition(0, 0)
} else {
getAbsolutePosition(item, row)
}
}
override fun swapDirectory(
@ -285,7 +316,7 @@ class GalleryAdapter(
}
override fun getFilesCount(): Int {
return files.fold(0) { acc, item -> acc + item.files.size }
return files.fold(0) { acc, item -> acc + item.rows.size }
}
@SuppressLint("NotifyDataSetChanged")
@ -302,4 +333,8 @@ class GalleryAdapter(
fun addFiles(items: List<GalleryItems>) {
files = items
}
fun changeColumn(newColumn: Int) {
columns = newColumn
}
}

View file

@ -22,35 +22,12 @@
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.GridImageBinding
class GalleryItemViewHolder(val binding: GridImageBinding) :
SectionedViewHolder(binding.root), ListGridImageViewHolder {
override val thumbnail: ImageView
SectionedViewHolder(binding.root) {
val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
}

View file

@ -0,0 +1,193 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.get
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.R
import com.owncloud.android.databinding.GalleryRowBinding
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.GalleryRow
import com.owncloud.android.datamodel.ImageDimension
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.DisplayUtils
class GalleryRowHolder(
val binding: GalleryRowBinding,
private val defaultThumbnailSize: Float,
private val ocFileListDelegate: OCFileListDelegate,
val storageManager: FileDataStorageManager,
private val galleryAdapter: GalleryAdapter
) : SectionedViewHolder(binding.root) {
val context = galleryAdapter.context
lateinit var currentRow: GalleryRow
fun bind(row: GalleryRow) {
currentRow = row
// re-use existing ones
while (binding.rowLayout.childCount < row.files.size) {
val shimmer = LoaderImageView(context).apply {
setImageResource(R.drawable.background)
resetLoader()
invalidate()
}
val imageView = ImageView(context).apply {
setImageDrawable(
ThumbnailsCacheManager.AsyncGalleryImageDrawable(
context.resources,
BitmapUtils.drawableToBitmap(
ResourcesCompat.getDrawable(resources, R.drawable.file_image, null),
defaultThumbnailSize.toInt(),
defaultThumbnailSize.toInt()
),
null
)
)
}
LinearLayout(context).apply {
addView(shimmer)
addView(imageView)
binding.rowLayout.addView(this)
}
}
if (binding.rowLayout.childCount > row.files.size) {
binding.rowLayout.removeViewsInLayout(row.files.size - 1, (binding.rowLayout.childCount - row.files.size))
}
val shrinkRatio = computeShrinkRatio(row)
for (indexedFile in row.files.withIndex()) {
adjustFile(indexedFile, shrinkRatio, row)
}
}
fun redraw() {
bind(currentRow)
}
@SuppressWarnings("MagicNumber", "ComplexMethod")
private fun computeShrinkRatio(row: GalleryRow): Float {
val screenWidth =
DisplayUtils.convertDpToPixel(context.resources.configuration.screenWidthDp.toFloat(), context)
.toFloat()
if (row.files.size > 1) {
var newSummedWidth = 0f
for (file in row.files) {
// first adjust all thumbnails to max height
val thumbnail1 = file.imageDimension ?: ImageDimension(defaultThumbnailSize, defaultThumbnailSize)
val height1 = thumbnail1.height
val width1 = thumbnail1.width
val scaleFactor1 = row.getMaxHeight() / height1
val newHeight1 = height1 * scaleFactor1
val newWidth1 = width1 * scaleFactor1
file.imageDimension = ImageDimension(newWidth1, newHeight1)
newSummedWidth += newWidth1
}
var c = 1f
// this ensures that files in last row are better visible,
// e.g. when 2 images are there, it uses 2/5 of screen
if (galleryAdapter.columns == 5) {
when (row.files.size) {
2 -> {
c = 5 / 2f
}
3 -> {
c = 4 / 3f
}
4 -> {
c = 4 / 5f
}
5 -> {
c = 1f
}
}
}
return (screenWidth / c) / newSummedWidth
} else {
val thumbnail1 = row.files[0].imageDimension ?: ImageDimension(defaultThumbnailSize, defaultThumbnailSize)
return (screenWidth / galleryAdapter.columns) / thumbnail1.width
}
}
private fun adjustFile(indexedFile: IndexedValue<OCFile>, shrinkRatio: Float, row: GalleryRow) {
val file = indexedFile.value
val index = indexedFile.index
val adjustedHeight1 = ((file.imageDimension?.height ?: defaultThumbnailSize) * shrinkRatio).toInt()
val adjustedWidth1 = ((file.imageDimension?.width ?: defaultThumbnailSize) * shrinkRatio).toInt()
// re-use existing one
val linearLayout = binding.rowLayout[index] as LinearLayout
val shimmer = linearLayout[0] as LoaderImageView
val thumbnail = linearLayout[1] as ImageView
thumbnail.adjustViewBounds = true
thumbnail.scaleType = ImageView.ScaleType.FIT_CENTER
ocFileListDelegate.bindGalleryRowThumbnail(
shimmer,
thumbnail,
file,
this,
adjustedWidth1
)
val params = LinearLayout.LayoutParams(adjustedWidth1, adjustedHeight1)
val zero = context.resources.getInteger(R.integer.zero)
val margin = context.resources.getInteger(R.integer.small_margin)
if (index < (row.files.size - 1)) {
params.setMargins(zero, zero, margin, margin)
} else {
params.setMargins(zero, zero, zero, margin)
}
thumbnail.layoutParams = params
thumbnail.layoutParams.height = adjustedHeight1
thumbnail.layoutParams.width = adjustedWidth1
shimmer.layoutParams = params
shimmer.layoutParams.height = adjustedHeight1
shimmer.layoutParams.width = adjustedWidth1
}
}

View file

@ -27,6 +27,7 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView
interface ListGridImageViewHolder {
val thumbnail: ImageView
fun showVideoOverlay()
val shimmerThumbnail: LoaderImageView
val favorite: ImageView
val localFileIndicator: ImageView

View file

@ -23,11 +23,14 @@ package com.owncloud.android.ui.adapter
import android.content.Context
import android.view.View
import android.widget.ImageView
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.ComponentsGetter
@ -54,6 +57,7 @@ class OCFileListDelegate(
private var highlightedItem: OCFile? = null
var isMultiSelect = false
private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
private val asyncGalleryTasks: MutableList<ThumbnailsCacheManager.GalleryImageGenerationTask> = ArrayList()
fun setHighlightedItem(highlightedItem: OCFile?) {
this.highlightedItem = highlightedItem
}
@ -87,6 +91,34 @@ class OCFileListDelegate(
checkedFiles.clear()
}
fun bindGalleryRowThumbnail(
shimmer: LoaderImageView?,
imageView: ImageView,
file: OCFile,
galleryRowHolder: GalleryRowHolder,
width: Int
) {
// thumbnail
imageView.tag = file.fileId
DisplayUtils.setGalleryImage(
file,
imageView,
user,
storageManager,
asyncGalleryTasks,
gridView,
context,
shimmer,
preferences,
themeColorUtils,
themeDrawableUtils,
galleryRowHolder,
width
)
imageView.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
}
fun bindGridViewHolder(
gridViewHolder: ListGridImageViewHolder,
file: OCFile,

View file

@ -21,35 +21,14 @@
*/
package com.owncloud.android.ui.adapter
import android.view.View
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.owncloud.android.databinding.GridImageBinding
internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
RecyclerView.ViewHolder(
binding.root
),
ListGridImageViewHolder {
override val thumbnail: ImageView
) {
val thumbnail: ImageView
get() = binding.thumbnail
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView
get() = binding.favoriteAction
override val localFileIndicator: ImageView
get() = binding.localFileIndicator
override val shared: ImageView
get() = binding.sharedIcon
override val checkbox: ImageView
get() = binding.customCheckbox
override val itemLayout: View
get() = binding.ListItemLayout
override val unreadComments: ImageView
get() = binding.unreadComments
init {
binding.favoriteAction.drawable.mutate()
}
}

View file

@ -37,6 +37,11 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
get() = binding.Filename
override val thumbnail: ImageView
get() = binding.thumbnail
override fun showVideoOverlay() {
binding.videoOverlay.visibility = View.VISIBLE
}
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView

View file

@ -48,6 +48,11 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
get() = binding.Filename
override val thumbnail: ImageView
get() = binding.thumbnail
override fun showVideoOverlay() {
binding.videoOverlay.visibility = View.VISIBLE
}
override val shimmerThumbnail: LoaderImageView
get() = binding.thumbnailShimmer
override val favorite: ImageView

View file

@ -232,7 +232,7 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
if (thumbnail != null) {
if (MimeTypeUtil.isVideo(file)) {
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail, context);
thumbnailView.setImageBitmap(withOverlay);
} else {
thumbnailView.setImageBitmap(thumbnail);

View file

@ -373,7 +373,7 @@ public class ExtendedListFragment extends Fragment implements
}
}
private void setGridViewColumns(float scaleFactor) {
protected void setGridViewColumns(float scaleFactor) {
if (mRecyclerView.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
if (mScale == -1f) {

View file

@ -25,6 +25,7 @@ package com.owncloud.android.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -38,6 +39,7 @@ import com.nextcloud.utils.view.FastScrollUtils;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.FolderPickerActivity;
@ -75,6 +77,9 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
@Inject FileDataStorageManager fileDataStorageManager;
@Inject FastScrollUtils fastScrollUtils;
private final int maxColumnSizeLandscape = 5;
private final int maxColumnSizePortrait = 2;
private int columnSize;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -86,6 +91,12 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
if (galleryFragmentBottomSheetDialog == null) {
galleryFragmentBottomSheetDialog = new GalleryFragmentBottomSheetDialog(this);
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
columnSize = maxColumnSizeLandscape;
} else {
columnSize = maxColumnSizePortrait;
}
}
@Override
@ -138,12 +149,14 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
this,
preferences,
mContainerActivity,
viewThemeUtils);
viewThemeUtils,
columnSize,
ThumbnailsCacheManager.getThumbnailDimension());
setRecyclerViewAdapter(mAdapter);
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), getColumnsCount());
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 1);
mAdapter.setLayoutManager(layoutManager);
getRecyclerView().setLayoutManager(layoutManager);
@ -152,6 +165,23 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
new GalleryFastScrollViewHelper(getRecyclerView(), mAdapter));
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
columnSize = maxColumnSizeLandscape;
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
columnSize = maxColumnSizePortrait;
}
mAdapter.changeColumn(columnSize);
showAllGalleryItems();
}
public int getColumnsCount() {
return columnSize;
}
@Override
public void onRefresh() {
super.onRefresh();
@ -236,7 +266,7 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
startDate = endDate - (daySpan * 24 * 60 * 60);
runGallerySearchTask();
// runGallerySearchTask();
}
@Override
@ -360,4 +390,9 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
}
});
}
@Override
protected void setGridViewColumns(float scaleFactor) {
// do nothing
}
}

View file

@ -123,7 +123,7 @@ class GalleryFastScrollViewHelper(
val adapter = mView.adapter as GalleryAdapter
if (adapter.sectionCount == 0) return 0
// in each section, the final row may contain less than the max of items
return adapter.files.sumOf { itemCountToRowCount(it.files.size) }
return adapter.files.sumOf { itemCountToRowCount(it.rows.size) }
}
/**
@ -141,7 +141,7 @@ class GalleryFastScrollViewHelper(
val seenRowsInPreviousSections = adapter.files
.subList(0, min(itemCoord.section(), adapter.files.size))
.sumOf { itemCountToRowCount(it.files.size) }
.sumOf { itemCountToRowCount(it.rows.size) }
val seenRowsInThisSection = if (isHeader) 0 else itemCountToRowCount(itemCoord.relativePos())
val totalSeenRows = seenRowsInPreviousSections + seenRowsInThisSection
@ -209,7 +209,7 @@ class GalleryFastScrollViewHelper(
*/
private fun getSectionStartOffsets(files: List<GalleryItems>): List<Int> {
val sectionHeights =
files.map { headerHeight + itemCountToRowCount(it.files.size) * rowHeight }
files.map { headerHeight + itemCountToRowCount(it.rows.size) * rowHeight }
val sectionStartOffsets = sectionHeights.indices.map { i ->
when (i) {
0 -> 0

View file

@ -378,7 +378,8 @@ public final class BitmapUtils {
return drawableToBitmap(drawable, -1, -1);
}
public static Bitmap drawableToBitmap(Drawable drawable, int desiredWidth, int desiredHeight) {
public static @NonNull
Bitmap drawableToBitmap(Drawable drawable, int desiredWidth, int desiredHeight) {
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if (bitmapDrawable.getBitmap() != null) {

View file

@ -34,7 +34,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
@ -74,6 +76,7 @@ import com.owncloud.android.lib.common.OwnCloudAccount;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.TextDrawable;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.adapter.GalleryRowHolder;
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
import com.owncloud.android.ui.events.SearchEvent;
import com.owncloud.android.ui.fragment.OCFileListFragment;
@ -752,6 +755,13 @@ public final class DisplayUtils {
return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
public static float convertPixelToDp(int px, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return px * (DisplayMetrics.DENSITY_DEFAULT / (float) metrics.densityDpi);
}
static public void showServerOutdatedSnackbar(Activity activity, int length) {
Snackbar.make(activity.findViewById(android.R.id.content),
R.string.outdated_server, length)
@ -811,13 +821,109 @@ public final class DisplayUtils {
}
}
public static String getDateByPattern(long timestamp, Context context, String pattern) {
DateFormat df = new SimpleDateFormat(pattern, context.getResources().getConfiguration().locale);
public static String getDateByPattern(long timestamp, String pattern) {
return getDateByPattern(timestamp, null, pattern);
}
public static String getDateByPattern(long timestamp, @Nullable Context context, String pattern) {
DateFormat df;
if (context == null) {
context = MainApp.getAppContext();
}
df = new SimpleDateFormat(pattern, context.getResources().getConfiguration().locale);
df.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
return df.format(timestamp);
}
public static void setGalleryImage(OCFile file,
ImageView thumbnailView,
User user,
FileDataStorageManager storageManager,
List<ThumbnailsCacheManager.GalleryImageGenerationTask> asyncTasks,
boolean gridView,
Context context,
LoaderImageView shimmerThumbnail,
AppPreferences preferences,
ThemeColorUtils themeColorUtils,
ThemeDrawableUtils themeDrawableUtils,
GalleryRowHolder galleryRowHolder,
Integer width) {
// cancel previous generation, if view is re-used
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
for (ThumbnailsCacheManager.GalleryImageGenerationTask task : asyncTasks) {
if (file.getRemoteId() != null && task.getImageKey() != null &&
file.getRemoteId().equals(task.getImageKey())) {
return;
}
}
try {
final ThumbnailsCacheManager.GalleryImageGenerationTask task =
new ThumbnailsCacheManager.GalleryImageGenerationTask(
thumbnailView,
user,
storageManager,
asyncTasks,
file.getRemoteId(),
context.getResources().getColor(R.color.bg_default));
Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
file.getFileName(),
user,
context,
themeColorUtils,
themeDrawableUtils);
if (drawable == null) {
drawable = ResourcesCompat.getDrawable(context.getResources(),
R.drawable.file_image,
null);
}
if (drawable == null) {
drawable = new ColorDrawable(Color.GRAY);
}
Bitmap thumbnail = BitmapUtils.drawableToBitmap(drawable, width / 2, width / 2);
final ThumbnailsCacheManager.AsyncGalleryImageDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncGalleryImageDrawable(context.getResources(),
thumbnail,
task);
if (shimmerThumbnail != null) {
Log_OC.d("Shimmer", "start Shimmer");
startShimmer(shimmerThumbnail, thumbnailView);
}
task.setListener(new ThumbnailsCacheManager.GalleryImageGenerationTask.GalleryListener() {
@Override
public void onSuccess() {
galleryRowHolder.getBinding().rowLayout.invalidate();
Log_OC.d("Shimmer", "stop Shimmer");
stopShimmer(shimmerThumbnail, thumbnailView);
}
@Override
public void onNewGalleryImage() {
galleryRowHolder.redraw();
}
@Override
public void onError() {
Log_OC.d("Shimmer", "stop Shimmer");
stopShimmer(shimmerThumbnail, thumbnailView);
}
});
thumbnailView.setImageDrawable(asyncDrawable);
asyncTasks.add(task);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
file);
} catch (IllegalArgumentException e) {
Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
}
}
}
public static void setThumbnail(OCFile file,
ImageView thumbnailView,
@ -850,7 +956,7 @@ public final class DisplayUtils {
stopShimmer(shimmerThumbnail, thumbnailView);
if (MimeTypeUtil.isVideo(file)) {
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail, context);
thumbnailView.setImageBitmap(withOverlay);
} else {
if (gridView) {
@ -886,6 +992,10 @@ public final class DisplayUtils {
R.drawable.file_image,
null);
}
if (drawable == null) {
drawable = new ColorDrawable(Color.GRAY);
}
int px = ThumbnailsCacheManager.getThumbnailDimension();
thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px);
}

View file

@ -0,0 +1,10 @@
<!-- drawable/video.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="32dp"
android:width="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="11dp"
android:height="14dp"
android:viewportWidth="11"
android:viewportHeight="14">
<path
android:pathData="M10.9799,6.9947L-0.0057,13.967L0.0004,0.0153L10.9799,6.9947Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -33,7 +33,8 @@
android:id="@id/exo_prev"
style="@style/FullScreenExoControlButton"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/exo_controls_previous" />
android:src="@drawable/exo_controls_previous"
android:contentDescription="@string/exo_controls_previous_description" />
<ImageButton
android:id="@id/exo_rew"

View file

@ -33,6 +33,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_half_margin"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textStyle="bold"
@ -43,6 +44,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_margin"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:text="2016" />
</LinearLayout>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />

View file

@ -15,87 +15,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ListItemLayout"
android:layout_width="match_parent"
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_horizontal"
android:orientation="vertical">
android:layout_margin="10dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
android:src="@drawable/file_image" />
<com.elyeproj.loaderviewlibrary.LoaderImageView
android:id="@+id/thumbnail_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/grid_image_icon_margin"
android:contentDescription="@null"
android:visibility="gone"
app:corners="6"
app:height_weight="0.6"
app:width_weight="0.4" />
<com.owncloud.android.ui.SquareImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:padding="@dimen/grid_image_icon_padding"
android:scaleType="centerCrop"
android:src="@drawable/file_image" />
<ImageView
android:id="@+id/favorite_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="@dimen/standard_quarter_margin"
android:contentDescription="@string/favorite_icon"
android:visibility="gone"
android:src="@drawable/favorite" />
<ImageView
android:id="@+id/sharedIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="@dimen/grid_image_shared_icon_layout_top_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:contentDescription="@string/shared_icon_shared_via_link"
android:src="@drawable/shared_via_link" />
<ImageView
android:id="@+id/unreadComments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="@dimen/grid_image_shared_icon_layout_top_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:clickable="true"
android:contentDescription="@string/unread_comments"
android:focusable="true"
android:src="@drawable/ic_comment_grid"
android:visibility="gone" />
<ImageView
android:id="@+id/localFileIndicator"
android:layout_width="@dimen/grid_image_local_file_indicator_layout_width"
android:layout_height="@dimen/grid_image_local_file_indicator_layout_height"
android:layout_gravity="bottom|end"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:layout_marginBottom="@dimen/standard_quarter_margin"
android:contentDescription="@string/synced_icon"
android:src="@drawable/ic_synced" />
<ImageView
android:id="@+id/custom_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|top"
android:layout_marginLeft="@dimen/standard_quarter_margin"
android:layout_marginRight="@dimen/standard_quarter_margin"
android:contentDescription="@string/checkbox"
android:src="@android:drawable/checkbox_off_background" />
</FrameLayout>

View file

@ -17,6 +17,7 @@
-->
<com.owncloud.android.ui.SquareLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ListItemLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -55,13 +56,30 @@
android:visibility="gone"
app:corners="8" />
<ImageView
android:id="@+id/thumbnail"
<FrameLayout
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/folder" />
android:layout_gravity="center">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/standard_list_item_size"
android:layout_height="@dimen/standard_list_item_size"
android:contentDescription="@null"
android:src="@drawable/folder" />
<ImageView
android:id="@+id/videoOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="4dp"
android:src="@drawable/video_white"
android:visibility="gone"
tools:visibility="visible"
android:contentDescription="@string/video_overlay_icon" />
</FrameLayout>
<ImageView
android:id="@+id/sharedIcon"
@ -71,7 +89,7 @@
android:layout_marginTop="@dimen/grid_item_shared_icon_layout_top_margin"
android:layout_marginEnd="@dimen/standard_quarter_margin"
android:src="@drawable/shared_via_link"
android:contentDescription="@string/shared_icon_shared_via_link"/>
android:contentDescription="@string/shared_icon_shared_via_link" />
<ImageView
android:id="@+id/unreadComments"

View file

@ -42,12 +42,29 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/thumbnail"
<FrameLayout
android:layout_width="@dimen/file_icon_size"
android:layout_height="@dimen/file_icon_size"
android:contentDescription="@null"
android:src="@drawable/folder" />
android:layout_gravity="center">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/file_icon_size"
android:layout_height="@dimen/file_icon_size"
android:contentDescription="@null"
android:src="@drawable/folder" />
<ImageView
android:id="@+id/videoOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginStart="2dp"
android:src="@drawable/video_white"
android:visibility="gone"
tools:visibility="visible"
android:contentDescription="@string/video_overlay_icon" />
</FrameLayout>
<com.elyeproj.loaderviewlibrary.LoaderImageView
android:id="@+id/thumbnail_shimmer"

View file

@ -106,11 +106,6 @@
<dimen name="contactlist_item_icon_layout_height">40dp</dimen>
<dimen name="empty_list_icon_layout_width">72dp</dimen>
<dimen name="empty_list_icon_layout_height">72dp</dimen>
<dimen name="grid_image_shared_icon_layout_top_margin">24dp</dimen>
<dimen name="grid_image_local_file_indicator_layout_width">16dp</dimen>
<dimen name="grid_image_local_file_indicator_layout_height">16dp</dimen>
<dimen name="grid_image_icon_margin">14dp</dimen>
<dimen name="grid_image_icon_padding">14dp</dimen>
<dimen name="grid_item_shared_icon_layout_top_margin">24dp</dimen>
<dimen name="grid_item_local_file_indicator_layout_width">16dp</dimen>
<dimen name="grid_item_local_file_indicator_layout_height">16dp</dimen>
@ -145,5 +140,6 @@
<dimen name="default_login_width">400dp</dimen>
<dimen name="dialogBorderRadius">24dp</dimen>
<dimen name="dialog_padding">24dp</dimen>
<integer name="small_margin">5</integer>
<integer name="zero">0</integer>
</resources>

View file

@ -1041,6 +1041,7 @@
<string name="file_already_exists">Filename already exists</string>
<string name="filedetails_export">Export</string>
<string name="locate_folder">Locate folder</string>
<string name="video_overlay_icon">video overlay icon</string>
<string name="app_widget_description">Shows one widget from dashboard</string>
<string name="icon_of_dashboard_widget">Icon of dashboard widget</string>
<string name="refresh_content">Refresh content</string>

View file

@ -27,6 +27,7 @@ import com.nextcloud.client.account.User
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.GalleryItems
import com.owncloud.android.datamodel.GalleryRow
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
@ -77,6 +78,7 @@ class GalleryAdapterTest {
@Test
fun testItemCount() {
whenever(transferServiceGetter.storageManager) doReturn storageManager
val thumbnailSize = 50
val sut = GalleryAdapter(
context,
@ -84,16 +86,24 @@ class GalleryAdapterTest {
ocFileListFragmentInterface,
preferences,
transferServiceGetter,
viewThemeUtils
viewThemeUtils,
5,
thumbnailSize
)
val list = listOf(
GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md"))),
GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md")))
GalleryItems(
1649317247,
listOf(GalleryRow(listOf(OCFile("/1.md"), OCFile("/2.md")), thumbnailSize, thumbnailSize))
),
GalleryItems(
1649317248,
listOf(GalleryRow(listOf(OCFile("/1.md"), OCFile("/2.md")), thumbnailSize, thumbnailSize))
)
)
sut.addFiles(list)
assertEquals(4, sut.getFilesCount())
assertEquals(2, sut.getFilesCount())
}
}

View file

@ -1,5 +1,6 @@
#Wed Oct 12 12:37:36 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME