Jelajahi Sumber

Add tile view to media view

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 2 tahun lalu
induk
melakukan
66d8756bec
39 mengubah file dengan 1152 tambahan dan 308 penghapusan
  1. 131 0
      app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt
  2. 37 0
      app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt
  3. 1 1
      app/src/main/java/com/owncloud/android/MainApp.java
  4. 7 0
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  5. 15 1
      app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt
  6. 29 0
      app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt
  7. 24 0
      app/src/main/java/com/owncloud/android/datamodel/ImageDimension.kt
  8. 13 0
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  9. 299 108
      app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
  10. 4 2
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  11. 5 0
      app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  12. 19 0
      app/src/main/java/com/owncloud/android/providers/FileContentProvider.java
  13. 1 1
      app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java
  14. 56 21
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt
  15. 2 25
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt
  16. 193 0
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt
  17. 1 0
      app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt
  18. 32 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  19. 2 23
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt
  20. 5 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt
  21. 5 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt
  22. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java
  23. 1 1
      app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  24. 38 3
      app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java
  25. 3 3
      app/src/main/java/com/owncloud/android/ui/fragment/util/GalleryFastScrollViewHelper.kt
  26. 2 1
      app/src/main/java/com/owncloud/android/utils/BitmapUtils.java
  27. 113 3
      app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
  28. 10 0
      app/src/main/res/drawable/video_white.xml
  29. 0 9
      app/src/main/res/drawable/view_play.xml
  30. 2 1
      app/src/main/res/layout/exo_player_control_view.xml
  31. 2 0
      app/src/main/res/layout/gallery_header.xml
  32. 27 0
      app/src/main/res/layout/gallery_row.xml
  33. 7 82
      app/src/main/res/layout/grid_image.xml
  34. 24 6
      app/src/main/res/layout/grid_item.xml
  35. 21 4
      app/src/main/res/layout/list_item.xml
  36. 2 6
      app/src/main/res/values/dims.xml
  37. 1 0
      app/src/main/res/values/strings.xml
  38. 14 4
      app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt
  39. 3 2
      gradle/wrapper/gradle-wrapper.properties

+ 131 - 0
app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt

@@ -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}")
+    }
+}

+ 37 - 0
app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt

@@ -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())
+    }
+}

+ 1 - 1
app/src/main/java/com/owncloud/android/MainApp.java

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

+ 7 - 0
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -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;

+ 15 - 1
app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt

@@ -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"
+    }
+}

+ 29 - 0
app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt

@@ -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
+    }
+}

+ 24 - 0
app/src/main/java/com/owncloud/android/datamodel/ImageDimension.kt

@@ -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)

+ 13 - 0
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -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;
+    }
 }

+ 299 - 108
app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -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 playButton = BitmapUtils.drawableToBitmap(playButtonDrawable, px, px);
 
-        Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton,
-                                                             playButtonWidth,
-                                                             playButtonHeight,
-                                                             true);
+        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;
+    }
 }

+ 4 - 2
app/src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -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

+ 5 - 0
app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -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);
 

+ 19 - 0
app/src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -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));
+            }
         }
     }
 }

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java

@@ -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);

+ 56 - 21
app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt

@@ -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
+    }
 }

+ 2 - 25
app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt

@@ -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
 }

+ 193 - 0
app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt

@@ -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
+    }
+}

+ 1 - 0
app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt

@@ -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

+ 32 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -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,

+ 2 - 23
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt

@@ -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()
-    }
 }

+ 5 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt

@@ -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

+ 5 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt

@@ -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

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java

@@ -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);

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java

@@ -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) {

+ 38 - 3
app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java

@@ -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
+    }
 }

+ 3 - 3
app/src/main/java/com/owncloud/android/ui/fragment/util/GalleryFastScrollViewHelper.kt

@@ -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

+ 2 - 1
app/src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -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) {

+ 113 - 3
app/src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -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);
                             }

+ 10 - 0
app/src/main/res/drawable/video_white.xml

@@ -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>

+ 0 - 9
app/src/main/res/drawable/view_play.xml

@@ -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>

+ 2 - 1
app/src/main/res/layout/exo_player_control_view.xml

@@ -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"

+ 2 - 0
app/src/main/res/layout/gallery_header.xml

@@ -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>

+ 27 - 0
app/src/main/res/layout/gallery_row.xml

@@ -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" />

+ 7 - 82
app/src/main/res/layout/grid_image.xml

@@ -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>

+ 24 - 6
app/src/main/res/layout/grid_item.xml

@@ -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"

+ 21 - 4
app/src/main/res/layout/list_item.xml

@@ -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"

+ 2 - 6
app/src/main/res/values/dims.xml

@@ -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>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -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>

+ 14 - 4
app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt

@@ -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())
     }
 }

+ 3 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -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