Android - How can I make a symlink (or equivalent) inside /storage/emulated/0?

ANDROID STORAGE:

On Android 5:

/sdcard >S> /storage/emulated/legacy >S> /mnt/shell/emulated/0
/mnt/shell/emulated >E> /data/media

On Android 6+:

# USER-ID of current user in case of multiple users, normally "0"

# for apps
# VIEW is one of "read" or "write" and /storage to VIEW bind mount is inside a separate mount namespace for every app
/sdcard >S> /storage/self/primary
/storage/self >B> /mnt/user/USER-ID
/mnt/user/USER-ID/primary >S> /storage/emulated/USER-ID
/storage/emulated >B> /mnt/runtime/VIEW/emulated
/mnt/runtime/VIEW/emulated >E> /data/media

# for services/daemons/processes in root namespace
/sdcard >S> /storage/self/primary
/storage >B> /mnt/runtime/default
/mnt/runtime/default/self/primary >S> mnt/user/0/primary
/mnt/user/0/primary >S> /storage/emulated/0
/storage/emulated >B> /mnt/runtime/default/emulated
/mnt/runtime/default/emulated >E> /data/media

>S> for symlink, >E> for emulated and >B> for bind mount

In short, /sdcard points to /data/media/0 through FUSE or sdcardfs emulation. This is to restrict unauthorized access of apps/processes to private media on SD card. Read Android's Storage Journey.

SYMLINKS:

Now /sdcard is not a real but emulated storage which represents a FAT/vFAT/FAT32 filesystem (for backward compatibility and permission management) which doesn't support symlinks (and other things including *NIX permissions and ioctls like FS_IOC_FIEMAP). So the Option 1 and 3 of yours won't work whether you create symlink directly on emulated storage or try to emulate the symlink already created on ext4.

BIND MOUNT:

This is the commonly used alternate of symlink for FAT family of filesystems. What you have tried in Option 2 should work. This is what apps like Apps2SD do. But there is again a constraint: mount namespace. You need to bind mount in global/root mount namespace so that the mount is visible to all apps:

su -mm -c 'mount -o bind "/data/sdext2/AppData/WhatsApp Media" "/sdcard/WhatsApp/Media"'

On Android 6+ this needs to be bind mounted on each VIEW (default, read, write) separately for all apps to work.

You can make it permanent by setting Mount Namespace Mode to Global in Magisk or by disabling Mount Namespace Separation in SuperSU. For details see this answer.


RELATED:

  • What is "/storage/emulated/0"?
  • How to bind mount a folder inside /sdcard with correct permissions?