Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ keystore.properties
fastlane/fastlane.json
fastlane/report.xml
.kotlin/

.agents/
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# CLAUDE.md

This file provides guidance to AI Agents when working with code in this repository.

## Project Overview

Right Gallery is an Android gallery/photo viewer app (package: `com.goodwy.gallery`), forked from Simple Gallery. Written in Kotlin, it uses View Binding (not Compose), Room for local database, Glide for image loading, and ExoPlayer (Media3) for video playback.

## Build Commands

```bash
# Debug build (FOSS flavor)
./gradlew assembleFossDebug

# Debug build (Google Play flavor)
./gradlew assembleGplayDebug

# Release build
./gradlew assembleFossRelease
./gradlew assembleGplayRelease

# Lint check
./gradlew lint

# Detekt (static analysis)
./gradlew detekt
```

## Build Configuration

- **Min SDK**: 26, **Target SDK**: 34, **Compile SDK**: 35
- **Java/Kotlin target**: JVM 17
- **Product flavors**: `foss` and `gplay` (licensing dimension) — flavor-specific resources live in `app/src/foss/` and `app/src/gplay/`
- **Signing**: via `keystore.properties` file or `SIGNING_*` environment variables
- **In-app product IDs**: configured in `local.properties` (PRODUCT_ID_X1..X4, SUBSCRIPTION_ID_X1..X3, etc.)
- **Version info**: managed in `gradle.properties` (VERSION_NAME, VERSION_CODE, APP_ID)
- **Dependencies catalog**: `gradle/libs.versions.toml`

## Architecture

### Core dependency
The app depends heavily on `com.github.Goodwy:Goodwy-Commons` (referenced as `goodwy.commons`), which provides base activities, shared utilities, common UI components, and the commons configuration system. Many base classes (activities, helpers) come from this library.

### Key packages under `app/src/main/kotlin/com/goodwy/gallery/`

- **activities/**: All Activity classes. `SimpleActivity` is the base for most activities. Key activities: `MainActivity` (directory listing), `MediaActivity` (media grid within a folder), `ViewPagerActivity` (full-screen media viewer), `EditActivity` (image editor), `VideoPlayerActivity`
- **adapters/**: RecyclerView adapters — `DirectoryAdapter` (folder list), `MediaAdapter` (media grid), with custom binding helpers (`DirectoryItemBinding`, `MediaItemBinding`)
- **models/**: Room entities and data classes — `Directory`, `Medium`, `Favorite`, `DateTaken`, `Widget`, plus editor models (`CanvasOp`, `PaintOptions`, `MyPath`)
- **databases/**: `GalleryDatabase` — Room database (version 10) with DAOs for directories, media, widgets, date_takens, and favorites
- **interfaces/**: Room DAO interfaces (`DirectoryDao`, `MediumDao`, `FavoritesDao`, `DateTakensDao`, `WidgetsDao`) and listener interfaces
- **helpers/**: `Config` (SharedPreferences wrapper for app settings), `Constants` (shared constant values), `MediaFetcher` (scans filesystem for media files), image loading helpers
- **extensions/**: Kotlin extension functions on `Context`, `Activity`, `String`, `View`, etc.
- **fragments/**: `PhotoFragment` and `VideoFragment` for the ViewPager-based media viewer
- **views/**: Custom views — `EditorDrawCanvas`, `MediaSideScroll` (brightness/volume gesture), `InstantItemSwitch`
- **receivers/**: `BootCompletedReceiver`, `RefreshMediaReceiver`
- **jobs/**: `NewPhotoFetcher` (background job to detect new photos)

### Data flow
Media discovery flows through `MediaFetcher` → cached in Room (`MediumDao`/`DirectoryDao`) → displayed via adapters. The `Config` helper wraps SharedPreferences for all user settings (sort order, view type, grouping, etc.).
16 changes: 16 additions & 0 deletions app/src/main/kotlin/com/goodwy/gallery/App.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.goodwy.gallery

import android.content.ComponentCallbacks2
import com.bumptech.glide.Glide
import com.bumptech.glide.MemoryCategory
import com.github.ajalt.reprint.core.Reprint
import com.goodwy.commons.RightApp
import com.goodwy.commons.extensions.isRuStoreInstalled
Expand All @@ -23,4 +26,17 @@ class App : RightApp() {
override fun shutdown() {}
}).build())
}

override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
Glide.get(this).setMemoryCategory(MemoryCategory.LOW)
}
Glide.get(this).trimMemory(level)
}

override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).clearMemory()
}
}
99 changes: 78 additions & 21 deletions app/src/main/kotlin/com/goodwy/gallery/activities/EditActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.goodwy.gallery.extensions.getCompressionFormatFromUri
import com.goodwy.gallery.extensions.readExif
import com.goodwy.gallery.extensions.proposeNewFilePath
import com.goodwy.gallery.extensions.resolveUriScheme
import com.goodwy.gallery.extensions.rescanFolderMediaSync
import com.goodwy.gallery.extensions.showContentDescriptionOnLongClick
import com.goodwy.gallery.extensions.writeBitmapToCache
import com.goodwy.gallery.extensions.fixDateTaken
Expand Down Expand Up @@ -1017,13 +1018,67 @@ class EditActivity : SimpleActivity(), CanvasListener {
}
}

private fun finishCropResultForContent(uri: Uri) {
val result = Intent().apply {
data = uri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
private fun getPathFromUri(uri: Uri?): String? {
if (uri == null) {
return null
}

return when (uri.scheme) {
"file" -> uri.path
else -> getRealPathFromURI(uri)
}?.takeIf { it.isNotEmpty() }
}

private fun getFoldersToResync(savedPaths: Collection<String>): ArrayList<String> {
val folders = linkedSetOf<String>()

savedPaths.map { it.trim() }
.filter { it.isNotEmpty() }
.forEach { path ->
folders.add(path.getParentPath())
}

arrayOf(uri, saveUri, originalUri).forEach { currentUri ->
getPathFromUri(currentUri)?.getParentPath()?.let(folders::add)
}

return folders.filter { it.isNotEmpty() } as ArrayList<String>
}

private fun finalizeEditedMedia(
savedPaths: Collection<String>,
resultIntent: Intent,
completion: () -> Unit,
) {
val pathsToScan = savedPaths.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct() as ArrayList<String>
val foldersToResync = getFoldersToResync(pathsToScan)

val completeOnUi = {
runOnUiThread {
setResult(RESULT_OK, resultIntent)
completion()
}
}

val resyncFolders = {
ensureBackgroundThread {
foldersToResync.forEach { folderPath ->
applicationContext.rescanFolderMediaSync(folderPath)
}
completeOnUi()
}
}

if (pathsToScan.isNotEmpty()) {
rescanPaths(pathsToScan) {
fixDateTaken(pathsToScan, false)
resyncFolders()
}
} else {
resyncFolders()
}
setResult(RESULT_OK, result)
finish()
}

private fun freeMemory() {
Expand Down Expand Up @@ -1087,8 +1142,10 @@ class EditActivity : SimpleActivity(), CanvasListener {
}

writeExif(oldExif, file.toUri())
setResult(RESULT_OK, intent)
scanFinalPath(file.absolutePath)
finalizeEditedMedia(arrayListOf(file.absolutePath), intent) {
toast(com.goodwy.commons.R.string.file_saved)
finish()
}
}

private fun saveBitmapToContentUri(
Expand Down Expand Up @@ -1121,11 +1178,21 @@ class EditActivity : SimpleActivity(), CanvasListener {
out.flush()
writeExif(oldExif, uri)

runOnUiThread {
val realPath = getPathFromUri(uri)
val savedPaths = listOfNotNull(realPath)
val resultIntent = if (isCropCommit) {
Intent().apply {
data = uri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} else {
intent
}

finalizeEditedMedia(savedPaths, resultIntent) {
if (isCropCommit) {
finishCropResultForContent(uri)
finish()
} else {
setResult(RESULT_OK, intent)
toast(com.goodwy.commons.R.string.file_saved)
finish()
}
Expand All @@ -1143,16 +1210,6 @@ class EditActivity : SimpleActivity(), CanvasListener {
isEditingWithThirdParty = true
}

private fun scanFinalPath(path: String) {
val paths = arrayListOf(path)
rescanPaths(paths) {
fixDateTaken(paths, false)
setResult(RESULT_OK, intent)
toast(com.goodwy.commons.R.string.file_saved)
finish()
}
}

override fun toggleUndoVisibility(visible: Boolean) {
//bottom_draw_undo.beVisibleIf(visible)
binding.editorToolbar.menu.findItem(R.id.undo).isEnabled = visible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.goodwy.gallery.R
import com.goodwy.gallery.adapters.ManageFoldersAdapter
import com.goodwy.gallery.databinding.ActivityManageFoldersBinding
import com.goodwy.gallery.extensions.config
import com.goodwy.gallery.extensions.evictFoldersFromCache

class ExcludedFoldersActivity : SimpleActivity(), RefreshRecyclerViewListener {

Expand Down Expand Up @@ -77,6 +78,7 @@ class ExcludedFoldersActivity : SimpleActivity(), RefreshRecyclerViewListener {
) {
config.lastFilepickerPath = it
config.addExcludedFolder(it)
applicationContext.evictFoldersFromCache(listOf(it))
updateFolders()
}
}
Expand Down
30 changes: 30 additions & 0 deletions app/src/main/kotlin/com/goodwy/gallery/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener {
val otgPath = trimEnd('/')
config.OTGPath = otgPath
config.addIncludedFolder(otgPath)
applicationContext.warmIncludedFolderCaches(otgPath)
}
}
}
Expand Down Expand Up @@ -1727,6 +1728,35 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener {
}
}

override fun excludeDirectories(paths: Set<String>) {
val normalizedPaths = paths.map { it.trimEnd('/') }
.filter { it.isNotEmpty() }
.toSet()

if (normalizedPaths.isEmpty()) {
return
}

val shouldRemove: (Directory) -> Boolean = { directory ->
normalizedPaths.any { excludedPath ->
directory.path.equals(excludedPath, true) || directory.path.startsWith("$excludedPath/", true)
}
}

mDirs = mDirs.filterNot(shouldRemove) as ArrayList<Directory>
mDirsIgnoringSearch = mDirsIgnoringSearch.filterNot(shouldRemove) as ArrayList<Directory>

getRecyclerAdapter()?.let { adapter ->
val updatedDirs = adapter.dirs.filterNot(shouldRemove) as ArrayList<Directory>
runOnUiThread {
adapter.updateDirs(updatedDirs)
checkPlaceholderVisibility(updatedDirs)
}
}

applicationContext.evictFoldersFromCache(normalizedPaths)
}

private fun checkWhatsNewDialog() {
arrayListOf<Release>().apply {
add(Release(504, R.string.release_504))
Expand Down
30 changes: 16 additions & 14 deletions app/src/main/kotlin/com/goodwy/gallery/activities/MediaActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.speech.RecognizerIntent
import android.view.View
import android.view.ViewGroup
Expand Down Expand Up @@ -64,8 +65,8 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
private var mLastSearchedText = ""
private var mLatestMediaId = 0L
private var mLatestMediaDateId = 0L
private var mLastMediaHandler = Handler()
private var mTempShowHiddenHandler = Handler()
private var mLastMediaHandler = Handler(Looper.getMainLooper())
private var mTempShowHiddenHandler = Handler(Looper.getMainLooper())
private var mCurrAsyncTask: GetMediaAsynctask? = null
private var mZoomListener: MyRecyclerView.MyZoomListener? = null

Expand Down Expand Up @@ -138,6 +139,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
}

updateWidgets()
maybeRunMediaDbMaintenance()
setupTabs()
}

Expand Down Expand Up @@ -512,11 +514,6 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
// }
// }

if (mShowLoadingIndicator) {
binding.loadingIndicator.show()
mShowLoadingIndicator = false
}

binding.mediaMenu.updateTitle(if (mShowAll) resources.getString(com.goodwy.strings.R.string.library) else dirName)
getMedia()
setupLayoutManager()
Expand All @@ -540,7 +537,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
isAGetIntent = mIsGetImageIntent || mIsGetVideoIntent || mIsGetAnyIntent,
allowMultiplePicks = mAllowPickingMultiple,
path = mPath,
recyclerView = binding.mediaGrid,
mediaRecyclerView = binding.mediaGrid,
swipeRefreshLayout = binding.mediaRefreshLayout
) {
if (it is Medium && !isFinishing) {
Expand Down Expand Up @@ -763,6 +760,11 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
if (mLoadedInitialPhotos) {
startAsyncTask()
} else {
// Show spinner optimistically; hide immediately if cache has data
if (mShowLoadingIndicator) {
binding.loadingIndicator.show()
mShowLoadingIndicator = false
}
getCachedMedia(
mPath,
mIsGetVideoIntent && !mIsGetImageIntent,
Expand Down Expand Up @@ -1097,7 +1099,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {

private fun gotMedia(media: ArrayList<ThumbnailItem>, isFromCache: Boolean) {
mIsGettingMedia = false
checkLastMediaChanged()
if (!isFromCache) checkLastMediaChanged()
mMedia = media

runOnUiThread {
Expand All @@ -1110,7 +1112,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
binding.mediaEmptyTextPlaceholder.text = getString(R.string.no_media_with_filters)
}
binding.mediaFastscroller.beVisibleIf(binding.mediaEmptyTextPlaceholder.isGone())
if (!isFromCache) setupAdapter()
setupAdapter()
}

mLatestMediaId = getLatestMediaId()
Expand Down Expand Up @@ -1236,10 +1238,10 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener {
private fun setupTabsColor() {
val tabBackground = when {
isDynamicTheme() && !isSystemInDarkMode() -> getProperBackgroundColor()
isLightTheme() -> resources.getColor(R.color.tab_background_light)
isGrayTheme() -> resources.getColor(R.color.tab_background_gray)
isDarkTheme() -> resources.getColor(R.color.tab_background_dark)
isBlackTheme() -> resources.getColor(R.color.tab_background_black)
isLightTheme() -> androidx.core.content.ContextCompat.getColor(this, R.color.tab_background_light)
isGrayTheme() -> androidx.core.content.ContextCompat.getColor(this, R.color.tab_background_gray)
isDarkTheme() -> androidx.core.content.ContextCompat.getColor(this, R.color.tab_background_dark)
isBlackTheme() -> androidx.core.content.ContextCompat.getColor(this, R.color.tab_background_black)
else -> getSurfaceColor().adjustAlpha(0.95f)
}
binding.mainTopTabsBackground.backgroundTintList = ColorStateList.valueOf(tabBackground)
Expand Down
Loading