Skip to content

Gallery couldn't read row 592, col 20 #17162

Open

Description

@alperozturk96

Same here
Crashes when Media Tab should Open . Not Sometimes, everytime. Same with App Version before.

Cause of error

Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalStateException: Couldn't read row 592, col 20 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
    at android.database.CursorWindow.nativeGetLong(Native Method)
    at android.database.CursorWindow.getLong(CursorWindow.java:573)
    at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:78)
    at androidx.sqlite.driver.SupportSQLiteStatement$RowSQLiteStatement.getLong(SupportSQLiteStatement.android.kt:363)
    at com.nextcloud.client.database.dao.FileDao_Impl.getGalleryItemsSuspended$lambda$0(FileDao_Impl.kt:4365)
    at com.nextcloud.client.database.dao.FileDao_Impl$$ExternalSyntheticLambda19.invoke(D8$$SyntheticClass:0)
    at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend(DBUtil.kt:173)
    at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:8)
    at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:4)
    at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:59)
    at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:8)
    at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:4)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:42)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:164)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
    at androidx.room.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:59)
    at androidx.room.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:138)
    at androidx.room.RoomDatabase.useConnection(RoomDatabase.android.kt:619)
    at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend(DBUtil.android.kt:261)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652)
    at java.lang.Thread.run(Thread.java:1564)

App information

  • ID: com.nextcloud.client
  • Version: 340000090
  • Build flavor: gplay

Device information

  • Brand: Xiaomi
  • Device: dada
  • Model: 24129PN74G
  • Id: BP2A.250605.031.A3
  • Product: dada_eea

Firmware

  • SDK: 36
  • Release: 16
  • Incremental: OS3.0.301.0.WOCEUXM

Originally posted by @Gdo23678 in #17160

Activity

  1. Gdo23678 commented on Jun 19, 2026

    @Gdo23678

    Describe the bug

    I am still experiencing consistent crashes, even after updating to the new RC1. The crash seems to be related to the gallery view and a database CursorWindow limitation.

    To Reproduce

    Steps to reproduce the behavior:

    1. Open the Nextcloud app
    2. Navigate to the 'Media' tab and swipe Gallery or open Picture
    3. App crashes immediately.

    Expected behavior

    The app should load the items without crashing.

    Server Environment

    • Nextcloud Server Version: 34.0
    • Database: PostgreSQL 16
    • Reverse Proxy: SWAG

    Cause of error

    java Exception in thread "DefaultDispatcher-worker-2" java.lang.IllegalStateException: Couldn't read row 592, col 20 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it. at android.database.CursorWindow.nativeGetLong(Native Method) at android.database.CursorWindow.getLong(CursorWindow.java:573) at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:78) at androidx.sqlite.driver.SupportSQLiteStatement$RowSQLiteStatement.getLong(SupportSQLiteStatement.android.kt:363) at com.nextcloud.client.database.dao.FileDao_Impl.getGalleryItemsSuspended$lambda$0(FileDao_Impl.kt:4365) at com.nextcloud.client.database.dao.FileDao_Impl$$ExternalSyntheticLambda19.invoke(D8$$SyntheticClass:0) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend(DBUtil.kt:173) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:8) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:4) at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:59) at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:8) at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:4) at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:42) at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:164) at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) at androidx.room.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:59) at androidx.room.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:138) at androidx.room.RoomDatabase.useConnection(RoomDatabase.android.kt:619) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend(DBUtil.android.kt:261) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652) at java.lang.Thread.run(Thread.java:1564)

    App information

    • ID: com.nextcloud.client
    • Version: 340000151
    • Build flavor: gplay

    Device information

    • Brand: Xiaomi
    • Device: dada
    • Model: 24129PN74G
    • Id: BP2A.250605.031.A3
    • Product: dada_eea

    Firmware

    • SDK: 36
    • Release: 16
    • Incremental: OS3.0.301.0.WOCEUXM
  2. Gdo23678 commented on Jun 19, 2026

    @Gdo23678

    Just to add some more context: I checked my local logs, and the crash happens exactly when the app tries to load the gallery items into memory all at once. The query returns a massive list, which seems to exceed the CursorWindow buffer limit.

    Here is the exact log line right before the crash:
    2026-06-19T18:23:31.493+0200;D;FileDataStorageManager;getGalleryItems - query complete, list size: 61632

  3. Gdo23678 commented on Jun 19, 2026

    @Gdo23678

    @alperozturk96 I noticed this is heavily related to the OOM issue in the Gallery tab we had back in May (which you fixed in PR #16986).

    It seems the underlying issue of fetching all gallery items at once without paging is still present. With the recent database migration to Room (FileDao_Impl.kt) in v34, this massive query now crashes the SQLite CursorWindow before it even gets the chance to cause a Java Heap OOM.

    As mentioned in my other comment, my local log shows the query trying to load over 61k items at once:
    getGalleryItems - query complete, list size: 61632

    Maybe this connection helps to finally implement pagination for the media tab

  4. Gdo23678 commented on Jun 24, 2026

    @Gdo23678

    Also in the newest Release still present

    Cause of error

    Exception in thread "DefaultDispatcher-worker-7" java.lang.IllegalStateException: Couldn't read row 592, col 20 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
        at android.database.CursorWindow.nativeGetLong(Native Method)
        at android.database.CursorWindow.getLong(CursorWindow.java:573)
        at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:78)
        at androidx.sqlite.driver.SupportSQLiteStatement$RowSQLiteStatement.getLong(SupportSQLiteStatement.android.kt:363)
        at com.nextcloud.client.database.dao.FileDao_Impl.getGalleryItemsSuspended$lambda$0(FileDao_Impl.kt:4365)
        at com.nextcloud.client.database.dao.FileDao_Impl$$ExternalSyntheticLambda19.invoke(D8$$SyntheticClass:0)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend(DBUtil.kt:173)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:8)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:4)
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:59)
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:8)
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke(Unknown Source:4)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:42)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:164)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
        at androidx.room.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:59)
        at androidx.room.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:138)
        at androidx.room.RoomDatabase.useConnection(RoomDatabase.android.kt:619)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend(DBUtil.android.kt:261)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652)
        at java.lang.Thread.run(Thread.java:1564)

    App information

    • ID: com.nextcloud.client
    • Version: 340000190
    • Build flavor: gplay

    Device information

    • Brand: Xiaomi
    • Device: dada
    • Model: 24129PN74G
    • Id: BP2A.250605.031.A3
    • Product: dada_eea

    Firmware

    • SDK: 36
    • Release: 16
    • Incremental: OS3.0.301.0.WOCEUXM
  5. alperozturk96 commented on Jul 1, 2026

    @alperozturk96
    CollaboratorAuthor

    @alperozturk96 I noticed this is heavily related to the OOM issue in the Gallery tab we had back in May (which you fixed in PR #16986).

    It seems the underlying issue of fetching all gallery items at once without paging is still present. With the recent database migration to Room (FileDao_Impl.kt) in v34, this massive query now crashes the SQLite CursorWindow before it even gets the chance to cause a Java Heap OOM.

    As mentioned in my other comment, my local log shows the query trying to load over 61k items at once: getGalleryItems - query complete, list size: 61632

    Maybe this connection helps to finally implement pagination for the media tab

    Exactly after adjusting some allocation seems like still issue persists. Pagination needs to be added. I will discuss with the team. @tobiasKaminsky fyi.

  6. alperozturk96 commented on Jul 8, 2026

    @alperozturk96
    CollaboratorAuthor

    @tobiasKaminsky

    Main issue is loading entire gallery in one query. Limitation of the mFiles in adapter should not cause an issue and having a report like exceed max size of the List implementation is most likely not realistic scenario.

    Also every config change, media-state toggle showAllGalleryItems is called. This is also a aggravating factor.

    Another thing is filtering done after DB query this is also affects memory allocation.

    My suggestion:

    1. Have pagination, filtering from one DB query and create a PR for this. This solely should solve the problem.
    2. Re-think usage of showAllGalleryItems function via different PR and try to reduce/optimize if possible.
  7. tobiasKaminsky commented on Jul 15, 2026

    @tobiasKaminsky
    Member

    Then let us start with 1. ✔

  8. alperozturk96 commented on Jul 15, 2026

    @alperozturk96
    CollaboratorAuthor

    @Gdo23678 Could you please try with last generated QA APK?

    #17335

  9. linked a pull request that will close this issuefeat(gallery): pagination #17335on Jul 15, 2026
  10. Gdo23678 commented on Jul 16, 2026

    @Gdo23678

    @alperozturk96 I have installed and tested the QA APK from PR #17335.

    The main crash is completely gone. I can scroll through my items without the CursorWindow limit crashing the app. Also, rotating the screen works without causing any crashes, thank you.

    However, the pagination seems to have introduced an index/mapping issue with videos.
    When I directly tap on a video in the media grid, it does not play. Instead, it opens a random, static image (always the exact same random image from my gallery).

    If I open a normal image right before the video in the grid and then swipe to the video, the video loads and plays perfectly fine.
    So it seems like directly tapping a video passes a wrong index or cached item to the PreviewImageFragment / ViewPager, but swiping through the ViewPager keeps the pagination index intact.

    Logs don't show an explicit crash or exception for this, it just fails silently by showing the wrong item.

  11. alperozturk96 commented on Jul 22, 2026

    @alperozturk96
    CollaboratorAuthor

    @Gdo23678 Hey,

    I updated the PR and tested seems like okay. Could you also verify?

    Thank you.

  12. Gdo23678 commented on Jul 26, 2026

    @Gdo23678

    @alperozturk96 I've been testing the updated APK since yesterday.

    I can confirm that the video index issue is completely resolved. Tapping directly on a video now opens and plays it correctly. Pagination and scrolling remain stable without any crashes. thx

  13. Gdo23678 commented on Aug 27, 2026

    @Gdo23678

    Hi @alperozturk96, could you please reopen this issue?

    The exact same crash is still happening. I have tested both the recent version 340010151 (34.1.0) and the brand new official update 340010190 (34.1.1) that was released on the Play Store today (August 27, 2026). The CursorWindow limit is still being hit in both versions.

    It seems the pagination fix from PR #17335 either didn't make it into the final Play Store build, or there is another background process still fetching the whole gallery at once without pagination.

    Here are both crash reports proving the issue is present in both builds:

    Cause of error

    Exception in thread "DefaultDispatcher-worker-3" java.lang.IllegalStateException: Couldn't read row 592, col 20 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
        at android.database.CursorWindow.nativeGetLong(Native Method)
        at android.database.CursorWindow.getLong(CursorWindow.java:573)
        at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:78)
        at androidx.sqlite.driver.SupportSQLiteStatement$RowSQLiteStatement.getLong([SupportSQLiteStatement.android.kt:363](http://supportsqlitestatement.android.kt:363/))
        at com.nextcloud.client.database.dao.FileDao_Impl.getGalleryItemsSuspended$lambda$0(FileDao_Impl.kt:4365)
        at com.nextcloud.client.database.dao.FileDao_Impl$$ExternalSyntheticLambda19.invoke(D8$$SyntheticClass:0)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend([DBUtil.kt:173](http://dbutil.kt:173/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke([DBUtil.kt:8](http://dbutil.kt:8/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke([DBUtil.kt:4](http://dbutil.kt:4/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend([PassthroughConnectionPool.kt:59](http://passthroughconnectionpool.kt:59/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke([PassthroughConnectionPool.kt:8](http://passthroughconnectionpool.kt:8/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke([PassthroughConnectionPool.kt:4](http://passthroughconnectionpool.kt:4/))
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched([Undispatched.kt:66](http://undispatched.kt:66/))
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn([Undispatched.kt:43](http://undispatched.kt:43/))
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext([Builders.common.kt:497](http://builders.common.kt:497/))
        at kotlinx.coroutines.BuildersKt.withContext(dex-id-31121b340192384674c05724be12a8c16f860c05:1)
        at androidx.room.coroutines.PassthroughConnectionPool.useConnection([PassthroughConnectionPool.kt:59](http://passthroughconnectionpool.kt:59/))
        at androidx.room.RoomConnectionManager.useConnection([RoomConnectionManager.android.kt:138](http://roomconnectionmanager.android.kt:138/))
        at androidx.room.RoomDatabase.useConnection([RoomDatabase.android.kt:619](http://roomdatabase.android.kt:619/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend([DBUtil.android.kt:261](http://dbutil.android.kt:261/))
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith([ContinuationImpl.kt:34](http://continuationimpl.kt:34/))
        at kotlinx.coroutines.DispatchedTask.run([DispatchedTask.kt:100](http://dispatchedtask.kt:100/))
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:1572)

    App information

    • ID: com.nextcloud.client
    • Version: 340010151
    • Build flavor: gplay

    Device information

    • Brand: Xiaomi
    • Device: dada
    • Model: 24129PN74G
    • Id: BP2A.250605.031.A3
    • Product: dada_eea

    Firmware

    • SDK: 36
    • Release: 16
    • Incremental: OS3.0.303.0.WOCEUXM

    Cause of error

    Exception in thread "DefaultDispatcher-worker-8" java.lang.IllegalStateException: Couldn't read row 592, col 20 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
        at android.database.CursorWindow.nativeGetLong(Native Method)
        at android.database.CursorWindow.getLong(CursorWindow.java:573)
        at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:78)
        at androidx.sqlite.driver.SupportSQLiteStatement$RowSQLiteStatement.getLong([SupportSQLiteStatement.android.kt:363](http://supportsqlitestatement.android.kt:363/))
        at com.nextcloud.client.database.dao.FileDao_Impl.getGalleryItemsSuspended$lambda$0(FileDao_Impl.kt:4365)
        at com.nextcloud.client.database.dao.FileDao_Impl$$ExternalSyntheticLambda19.invoke(D8$$SyntheticClass:0)
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend([DBUtil.kt:173](http://dbutil.kt:173/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke([DBUtil.kt:8](http://dbutil.kt:8/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke([DBUtil.kt:4](http://dbutil.kt:4/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend([PassthroughConnectionPool.kt:59](http://passthroughconnectionpool.kt:59/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke([PassthroughConnectionPool.kt:8](http://passthroughconnectionpool.kt:8/))
        at androidx.room.coroutines.PassthroughConnectionPool$useConnection$2.invoke([PassthroughConnectionPool.kt:4](http://passthroughconnectionpool.kt:4/))
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched([Undispatched.kt:66](http://undispatched.kt:66/))
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn([Undispatched.kt:43](http://undispatched.kt:43/))
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext([Builders.common.kt:497](http://builders.common.kt:497/))
        at kotlinx.coroutines.BuildersKt.withContext(dex-id-31121b340192384674c05724be12a8c16f860c05:1)
        at androidx.room.coroutines.PassthroughConnectionPool.useConnection([PassthroughConnectionPool.kt:59](http://passthroughconnectionpool.kt:59/))
        at androidx.room.RoomConnectionManager.useConnection([RoomConnectionManager.android.kt:138](http://roomconnectionmanager.android.kt:138/))
        at androidx.room.RoomDatabase.useConnection([RoomDatabase.android.kt:619](http://roomdatabase.android.kt:619/))
        at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend([DBUtil.android.kt:261](http://dbutil.android.kt:261/))
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith([ContinuationImpl.kt:34](http://continuationimpl.kt:34/))
        at kotlinx.coroutines.DispatchedTask.run([DispatchedTask.kt:100](http://dispatchedtask.kt:100/))
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:1572)

    App information

    • ID: com.nextcloud.client
    • Version: 340010190
    • Build flavor: gplay

    Device information

    • Brand: Xiaomi
    • Device: dada
    • Model: 24129PN74G
    • Id: BP2A.250605.031.A3
    • Product: dada_eea

    Firmware

    • SDK: 36
    • Release: 16
    • Incremental: OS3.0.303.0.WOCEUXM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions