11

In my app, users pick files. Internally, I store information about the file, which I key based on the file path. Next time that file is used, I do stuff with the stored information. Trouble is I instantiate my files with:

File file1 = new File(Environment.getExternalStorageDirectory() + "/test.txt");

And then, on a particular JB device, file1.getCanonicalPath() gives: "/storage/emulated/0/test.txt".

The trouble is that when other apps launch my app with a file path in an Intent, the paths they send tend to look like: "/mnt/sdcard/test.txt".

Is there a smart strategy to disambiguate these two paths? Possibly I should be instantiating my files differently?

Edit:

The trouble is, the two canaonical paths for the two files are not equal. For the below, cp1=="mnt/sdcard/test/txt" and cp2=="/storage/emulated/0/text/txt":

File file1 = new File("/mnt/sdcard/test.txt");
File file2 = new File("/storage/emulated/0/test.txt");

String cp1 = file1.getCanonicalPath();
String cp2 = file2.getCanonicalPath();
ab11
  • 19,770
  • 42
  • 120
  • 207
  • The same file has 2 different absolute paths ? These paths are different and can exist on the storage without mutual exclusion. Can you elaborate on the scenario a little more ? How is the `Intent` sent to you prepared and when is it sent ? – Deepak Bala Apr 08 '13 at 17:04
  • I was hoping that there would be a way to equate the two, because the different paths point to the same file. Alternatively, if I evaluated Environment.getExternalStorageDirectory() as "/mnt/sdcard/" instead of as "/storage/emulated/0", it would reduce the occurance of this issue (but I don't know how to do this) – ab11 Apr 08 '13 at 17:14

4 Answers4

6

First, the only correct way to get the external path is using the getExternalStorageDirectory and other getExternalStorageXXX in Android.

Android will firstly try to resolve two system variable:

String rawExternalStorage = System.getenv(ENV_EXTERNAL_STORAGE);
String rawEmulatedStorageTarget = System.getenv(ENV_EMULATED_STORAGE_TARGET);

while the ENV_EXTERNAL_STORAGE = "EXTERNAL_STORAGE" and ENV_EMULATED_STORAGE_TARGET = "EMULATED_STORAGE_TARGET". If the EMULATED_STORAGE_TARGET variable is set, it means the device has emulated storage then the storage path will be EMULATED_STORAGE_TARGET.(After Android 4.2, it support multiple-user external storage, then there would be a /0 or 0 after the path) But if it is not set and EXTERNAL_STORAGE is set, the path will be EXTERNAL_STORAGE. If both of them are not set, the path will be /storage/sdcard0 by default. So different devices may contain different paths for external storage.

As External Storage Technical Information says, you can customize the storage of the device by setting up the init.rc file. For example in the default goldfish one:

export EXTERNAL_STORAGE /mnt/sdcard
mkdir /mnt/sdcard 0000 system system
symlink /mnt/sdcard /sdcard

If you use getExternalStorageDirectory you will get /mnt/sdcard, but /sdcard is a symbolic link to that directory.

So in your case, the init.rc may contain:

export EMULATED_STORAGE_TARGET /storage/emulated
symlink /storage/emulated/0 /mnt/sdcard

So they are not ambiguous, they are actually same.

I think the getCanonicalPath() might work for the vast majority of your use cases.

A canonical pathname is both absolute and unique. The precise definition of canonical form is system-dependent. This method first converts this pathname to absolute form if necessary, as if by invoking the getAbsolutePath() method, and then maps it to its unique form in a system-dependent way. This typically involves removing redundant names such as "." and ".." from the pathname, resolving symbolic links (on UNIX platforms), and converting drive letters to a standard case (on Microsoft Windows platforms).

Every pathname that denotes an existing file or directory has a unique canonical form. Every pathname that denotes a nonexistent file or directory also has a unique canonical form. The canonical form of the pathname of a nonexistent file or directory may be different from the canonical form of the same pathname after the file or directory is created. Similarly, the canonical form of the pathname of an existing file or directory may be different from the canonical form of the same pathname after the file or directory is deleted.

StarPinkER
  • 14,081
  • 7
  • 55
  • 81
  • 3
    trouble is, given two paths "/mnt/sdcard/test.txt" and "/storage/emulated/0/test.txt": File file1 = new File(path1), File file 2 = new File(path2). file1.getCanonicalPath().equals(file2.getCanoniclaPath()) evaluates to false, though they point to the same file. – ab11 Apr 09 '13 at 12:54
  • Are you sure they reference to a same file? Modifying one file will affect another? I tested on my device, it works. @ab11 – StarPinkER Apr 14 '13 at 03:12
  • FWIW, the more recent Android version you use, the worst it gets and that's not only valid on storages, but also other path like /sys folders! On Marshmallow, getCanonicalPath returns the original path most of the time, even though a ls -l shows the path to be a link. Only solution I found is to use a shell to run a ls -l or readlink command. – 3c71 Jan 17 '16 at 08:54
2

There is probably no easy solution for this. The problem is that apparently there are two different mount points in the file system that actually point to the same location. File.getCanonicalPath() can only resolve symbolic links but not different mount points.

For example on my Nexus 4 this code:

File file1 = new File(Environment.getExternalStorageDirectory() + "/Android");
System.out.println("file 1: " + file1.getCanonicalPath());
File file2 = new File("/sdcard/Android");
System.out.println("file 2: " + file2.getCanonicalPath());

prints

file 1: /storage/emulated/0/Android
file 2: /storage/emulated/legacy/Android

I used this code to exec "mount" and print the output:

Process exec = Runtime.getRuntime().exec("mount");
InputStream in = exec.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
while (true) {
    String line = br.readLine();
    if (line == null)
        break;
    System.out.println(line);
}
in.close();

The relevant output is:

/dev/fuse /storage/emulated/0 fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
/dev/fuse /storage/emulated/legacy fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
devconsole
  • 7,875
  • 1
  • 34
  • 42
0

Take a look at the answer here. It also uses canonical path, but in a slightly different manner that might work for you

Community
  • 1
  • 1
Hamy
  • 20,662
  • 15
  • 74
  • 102
  • If the folder had not yet been created, then getCanonicalPath() would probably return the same path as was passed to the File constructor, just as the OP is reporting for files. I think the key is probably to call the method after the file is created. – Carl Apr 15 '13 at 08:16
0

On newer Android versions, SD storage is available from many paths, for example:

/storage/emulated/0
/storage/emulated/legacy (root account most of the time)
/sdcard
/data/media

If you check on which device those paths are located, some are on different devices (because of fuse 'virtual' filesystem), hence getting their canonical path doesn't lead to the same file path, even though they are actually the same files.

Furthermore, it appears that on Marshmallow, things get worse and even file path under /sys (full of redirections/links) are not reported properly and getCanonicalPath() returns the original path instead of actual canonical path.

While a ls -l (or readlink) on the given file path will show the actual canonical path, the API doesn't work anymore. Unfortunately running a readlink or ls -l is very slow (average of 130ms when the shell is already running), compared to an already slow, but much faster getCanonicalPath(), it's a pity.

Conclusion, getCanonicalPath is broken and has always been broken.

3c71
  • 4,313
  • 31
  • 43