I’d say that most games have a need to save player progress and stats between play sessions; it’s pretty vital. Unity provides a PlayerPrefs class to save/load int, float, and string data types into a giant key/value pair database between games. This is handy, but not ideal in all cases. Sometimes, you want to save several individual files – maybe one file per profile, or separate files for audio settings, engine settings, etc.
When you want to save data between play sessions, you need to have a path to a “safe” location that is always available. Unity provides a variable called Application.persistentDataPath that returns a directory path to just such a location. It returns a different location for each platform – PC, iOS, Android, etc.
However, the way that Android handles data storage can cause some headaches when working with Unity’s persistentDataPath variable. In this article, I’ll explain how data storage works on Android, and why you need to be careful with persistentDataPath as a result.
How Does Android Store Apps and Data?
Android apps are stored on your device’s hard drive alongside a folder for that app’s data. This is the same way iOS apps are stored, but Android has an additional complexity: the SD card. When working on Android, you may hear the terms “internal storage” for the storage in the actual device, or “external storage” to refer to an SD card.
When external storage is unavailable, all apps and data are put on the device’s internal storage. If you use an app like Root Explorer Lite to explore the device’s file system, you’ll find that apps are saved to the “data/data/<bundleID>” directory, where bundleID is the app’s bundle ID (i.e. com.mycompany.mygame). This is very clean and neat, and is equivalent to iOS functionality.
When you add an SD card to your device, things get slightly confusing. Your app and your data can either be stored on internal or external storage. The external storage location is something along the lines of “mnt/sdcard/Android/data/<bundleID>”. Whether an app or app data is stored in either location depends on a number of factors, explained below!
Telling Android Where to Put Apps
When creating your app’s manifest, there is an attribute called android:installLocation that allows you to “suggest” to the Android OS where your app should be installed. There are three values you can use for different effects:
- preferExternal – When your app is installed, it will be installed to the SD card if there is space; if not, it will be put on internal storage. Users also have the ability to move the app between internal and external storage if they’d like.
- auto – When your app is installed, the OS will decide where to install the app based on certain metrics (that are not exposed). Users will also have the ability to move the app between internal and external storage if they’d like.
- Excluding this attribute from the manifest forces the app to be installed on internal storage. Users CANNOT move it to external storage.
Many devices have small amounts of internal storage, so it is a bad idea to exclude this property, especially with games that take up hundreds of megabytes of space. This means that you will give your users the option to install apps on external storage, and many will take that option to conserve valuable internal storage space.
Luckily, the app’s location is less of a concern than the data’s location. When an app is stored on external storage, it works just like any other app. The only downside is that if the SD card is missing or unmounted (for example, plugging your device into a computer via USB will unmount the SD card), your app will fail to run.
You can read more about the app’s install location in the official documentation.
Telling Android Where to Put Data
Regardless of whether your app is installed on internal or external storage, you are only able to save and load data from internal storage by default. The location most often used for saving such data is “data/data/<bundleID>/files”.
You can also give your app the permission to write to external storage. This is done by adding a permission to your app’s manifest with the key WRITE_EXTERNAL_STORAGE. When you do this, your app will have the ability to write to both the internal storage location listed above, or to an external location, most commonly “mnt/sdcard/Android/data/<bundleID>/files” (though you are technically given the permission to write to any location on external storage).
You lose the ability to read your data if the SD card is removed or unmounted. This presents a problem, since you will not be able to read or write any progress or settings file when the SD card is in this state – it will appear to players as though they have lost data, or their progress isn’t being saved. From personal experience, angry customer emails are likely.
The biggest problem with saving data on external storage is that users can connect via USB and then view or modify your data on their desktops. For things like achievement, progression, or in-game currency, this can allow your game to be easily hacked, especially if you store data in plain text format.
Oh, also devices running Android 2.2 will delete everything in the app’s external storage directory when the app is updated. This was a bug in the OS at the time, and clearly a pretty bad one – your 2.2 users won’t be happy!
For these reasons, I like to keep my game data on internal storage. It is difficult for users to access and modify, and it has a much greater guarantee of being available at any time.
Unity’s Save Data Pitfall
Now that we’ve got an idea of how data and apps are saved on Android, what could possibly go wrong with Unity? The problem stems from the fact that Unity provides one variable for getting a persistent data location, but Android provides up to two: internal and external. As explained in the Data Storage documentation, the Android SDK provides functions to get the internal and external data paths for your app – getFilesDir() and getExternalFilesDir(). This makes it quite clear which function to call to get which path, and you know where your data is being saved.
In Unity, there is only one variable Application.persistentDataPath. Unfortunately, the data location returned by this variable will change based on the WRITE_EXTERNAL_STORAGE permission, and whether an SD card is mounted:
- If your app does not have WRITE_EXTERNAL_STORAGE permission, this will always return the internal data path.
- If your app does have WRITE_EXTERNAL_STORAGE permission, and an SD card is available, it will return the external data path.
- If your app does have WRITE_EXTERNAL_STORAGE permission, and no SD card is available, it will return the internal data path.
For a variable claiming to be persistent, it changes quite a bit. The easiest way to avoid this problem is to not grant external storage permissions, but let’s say that you need it for some reason (maybe a plugin requires it).
There are two ways I know of to get around this problem. The first is to write a simple Java plugin for Unity that will just call either getFilesDir() or getExternalFilesDir() and return the value for you to use. Writing a plugin is a bit of a hassle, but this is probably the most correct thing to do, and will always give you a valid path on all Android versions.
The other option is to just construct the path within Unity. You know the paths to both locations; you just need to substitute in your bundle ID and you are set! This is somewhat bad practice because future versions of Android or Unity could conceivably change these path values, but it is much easier than writing a plugin.
//*** Constant data paths. private const string INTERNAL_DATA_PATH = "/data/data/com.mycompany.mygame/files"; private const string EXTERNAL_DATA_PATH = "/mnt/sdcard/Android/data/com.mycompany.mygame/files";