[android]: Use scoped storage

Signed-off-by: Roman Tsisyk <roman@tsisyk.com>
This commit is contained in:
Roman Tsisyk 2021-03-20 21:44:01 +03:00
parent 43c75200fa
commit 4822f1a39b
10 changed files with 161 additions and 207 deletions

View file

@ -94,7 +94,7 @@ Java_com_mapswithme_maps_downloader_MapManager_nativeGetRoot(JNIEnv * env, jclas
JNIEXPORT jboolean JNICALL
Java_com_mapswithme_maps_downloader_MapManager_nativeMoveFile(JNIEnv * env, jclass clazz, jstring oldFile, jstring newFile)
{
return base::RenameFileX(jni::ToNativeString(env, oldFile), jni::ToNativeString(env, newFile));
return base::MoveFileX(jni::ToNativeString(env, oldFile), jni::ToNativeString(env, newFile));
}
// static boolean nativeHasSpaceToDownloadAmount(long bytes);

View file

@ -7,9 +7,9 @@
extern "C"
{
// static void nativePreparePlatform(String settingsPath);
// static void nativeSetSettingsDir(String settingsPath);
JNIEXPORT void JNICALL
Java_com_mapswithme_maps_MwmApplication_nativePreparePlatform(JNIEnv * env, jclass clazz, jstring settingsPath)
Java_com_mapswithme_maps_MwmApplication_nativeSetSettingsDir(JNIEnv * env, jclass clazz, jstring settingsPath)
{
android::Platform::Instance().SetSettingsDir(jni::ToNativeString(env, settingsPath));
}
@ -18,12 +18,12 @@ extern "C"
// String obbGooglePath, String flavorName, String buildType, boolean isTablet);
JNIEXPORT void JNICALL
Java_com_mapswithme_maps_MwmApplication_nativeInitPlatform(JNIEnv * env, jobject thiz,
jstring apkPath, jstring storagePath,
jstring apkPath, jstring writablePath,
jstring privatePath, jstring tmpPath,
jstring flavorName, jstring buildType,
jboolean isTablet)
{
android::Platform::Instance().Initialize(env, thiz, apkPath, storagePath, privatePath, tmpPath,
android::Platform::Instance().Initialize(env, thiz, apkPath, writablePath, privatePath, tmpPath,
flavorName, buildType, isTablet);
}

View file

@ -137,7 +137,7 @@ platform::NetworkPolicy GetCurrentNetworkPolicy()
namespace android
{
void Platform::Initialize(JNIEnv * env, jobject functorProcessObject, jstring apkPath,
jstring storagePath, jstring privatePath, jstring tmpPath,
jstring writablePath, jstring privatePath, jstring tmpPath,
jstring flavorName, jstring buildType, bool isTablet)
{
m_functorProcessObject = env->NewGlobalRef(functorProcessObject);
@ -160,8 +160,7 @@ void Platform::Initialize(JNIEnv * env, jobject functorProcessObject, jstring ap
m_resourcesDir = jni::ToNativeString(env, apkPath);
m_privateDir = jni::ToNativeString(env, privatePath);
m_tmpDir = jni::ToNativeString(env, tmpPath);
m_writableDir = jni::ToNativeString(env, storagePath);
m_writableDir = jni::ToNativeString(env, writablePath);
LOG(LINFO, ("Apk path = ", m_resourcesDir));
LOG(LINFO, ("Writable path = ", m_writableDir));
LOG(LINFO, ("Temporary path = ", m_tmpDir));
@ -182,17 +181,6 @@ void Platform::OnExternalStorageStatusChanged(bool isAvailable)
{
}
std::string Platform::GetStoragePathPrefix() const
{
size_t const count = m_writableDir.size();
ASSERT_GREATER ( count, 2, () );
size_t const i = m_writableDir.find_last_of('/', count-2);
ASSERT_GREATER ( i, 0, () );
return m_writableDir.substr(0, i);
}
void Platform::SetWritableDir(std::string const & dir)
{
m_writableDir = dir;
@ -203,7 +191,8 @@ void Platform::SetWritableDir(std::string const & dir)
void Platform::SetSettingsDir(std::string const & dir)
{
m_settingsDir = dir;
LOG(LINFO, ("Settings path = ", m_settingsDir));
// Logger is not fully initialized here.
//LOG(LINFO, ("Settings path = ", m_settingsDir));
}
bool Platform::HasAvailableSpaceForWriting(uint64_t size) const

View file

@ -17,17 +17,14 @@ namespace android
class Platform : public ::Platform
{
public:
void Initialize(JNIEnv * env, jobject functorProcessObject, jstring apkPath, jstring storagePath,
jstring privatePath, jstring tmpPath, jstring flavorName, jstring buildType,
bool isTablet);
void Initialize(JNIEnv * env, jobject functorProcessObject, jstring apkPath, jstring writablePath,
jstring privatePath, jstring tmpPath, jstring flavorName,
jstring buildType, bool isTablet);
~Platform() override;
void OnExternalStorageStatusChanged(bool isAvailable);
/// get storage path without ending "/OMapsData/"
std::string GetStoragePathPrefix() const;
/// assign storage path (should contain ending "/OMapsData/")
void SetWritableDir(std::string const & dir);
void SetSettingsDir(std::string const & dir);

View file

@ -28,6 +28,7 @@ import com.mapswithme.maps.routing.RoutingController;
import com.mapswithme.maps.scheduling.ConnectivityJobScheduler;
import com.mapswithme.maps.scheduling.ConnectivityListener;
import com.mapswithme.maps.search.SearchEngine;
import com.mapswithme.maps.settings.StoragePathManager;
import com.mapswithme.maps.sound.TtsPlayer;
import com.mapswithme.util.Config;
import com.mapswithme.util.ConnectionState;
@ -132,6 +133,9 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
LoggerFactory.INSTANCE.initialize(this);
mLogger = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.MISC);
getLogger().d(TAG, "Application is created");
// Set configuration directory as early as possible.
// Other methods may explicitly use Config, which requires settingsDir to be set.
setSettingsDir();
mMainLoopHandler = new Handler(getMainLooper());
ConnectionState.INSTANCE.initialize(this);
CrashlyticsUtils.INSTANCE.initialize(this);
@ -156,6 +160,20 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
channelProvider.setDownloadingChannel();
}
/**
* Initialize configuration directory.
*/
public void setSettingsDir()
{
final String settingsPath = StorageUtils.getSettingsPath(this);
if (!StorageUtils.createDirectory(this, settingsPath))
{
throw new AssertionError("Can't create settingsDir " + settingsPath);
}
getLogger().d(TAG, "Settings path = " + settingsPath);
nativeSetSettingsDir(settingsPath);
}
/**
* Initialize native core of application: platform and framework. Caller must handle returned value
* and do nothing with native code if initialization is failed.
@ -177,24 +195,27 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
if (mPlatformInitialized)
return;
final String settingsPath = StorageUtils.getSettingsPath();
getLogger().d(TAG, "onCreate(), setting path = " + settingsPath);
final String filesPath = StorageUtils.getFilesPath(this);
getLogger().d(TAG, "onCreate(), files path = " + filesPath);
final Logger log = getLogger();
final String apkPath = StorageUtils.getApkPath(this);
log.d(TAG, "Apk path = " + apkPath);
// Note: StoragePathManager uses Config, which requires initConfig() to be called.
final String writablePath = new StoragePathManager().findMapsStorage(this);
log.d(TAG, "Writable path = " + writablePath);
final String privatePath = StorageUtils.getPrivatePath(this);
log.d(TAG, "Private path = " + privatePath);
final String tempPath = StorageUtils.getTempPath(this);
getLogger().d(TAG, "onCreate(), temp path = " + tempPath);
log.d(TAG, "Temp path = " + tempPath);
// If platform directories are not created it means that native part of app will not be able
// to work at all. So, we just ignore native part initialization in this case, e.g. when the
// external storage is damaged or not available (read-only).
if (!createPlatformDirectories(settingsPath, filesPath, tempPath))
if (!createPlatformDirectories(writablePath, privatePath, tempPath))
return;
// First we need initialize paths and platform to have access to settings and other components.
nativePreparePlatform(settingsPath);
nativeInitPlatform(StorageUtils.getApkPath(this),
StorageUtils.getStoragePath(settingsPath),
filesPath, tempPath,
nativeInitPlatform(apkPath,
writablePath,
privatePath,
tempPath,
BuildConfig.FLAVOR,
BuildConfig.BUILD_TYPE, UiUtils.isTablet(this));
@ -204,15 +225,15 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
mPlatformInitialized = true;
}
private boolean createPlatformDirectories(@NonNull String settingsPath,
@NonNull String filesPath,
private boolean createPlatformDirectories(@NonNull String writablePath,
@NonNull String privatePath,
@NonNull String tempPath)
{
if (SharedPropertiesUtils.shouldEmulateBadExternalStorage(this))
return false;
return StorageUtils.createDirectory(this, settingsPath) &&
StorageUtils.createDirectory(this, filesPath) &&
return StorageUtils.createDirectory(this, writablePath) &&
StorageUtils.createDirectory(this, privatePath) &&
StorageUtils.createDirectory(this, tempPath);
}
@ -308,8 +329,8 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
return mPlayer;
}
private static native void nativePreparePlatform(String settingsPath);
private native void nativeInitPlatform(String apkPath, String storagePath, String privatePath,
private static native void nativeSetSettingsDir(String settingsPath);
private native void nativeInitPlatform(String apkPath, String writablePath, String privatePath,
String tmpPath, String flavorName, String buildType,
boolean isTablet);
private static native void nativeInitFramework();

View file

@ -1,7 +1,5 @@
package com.mapswithme.maps.settings;
import com.mapswithme.util.Constants;
/**
* Represents storage option.
*/
@ -47,6 +45,11 @@ public class StorageItem
public String getFullPath()
{
return mPath + Constants.MWM_DIR_POSTFIX;
return mPath;
}
public long getFreeSize()
{
return mFreeSize;
}
}

View file

@ -1,6 +1,7 @@
package com.mapswithme.maps.settings;
import android.app.Activity;
import android.app.Application;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@ -8,8 +9,9 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@ -28,9 +30,11 @@ import com.mapswithme.util.log.LoggerFactory;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@ -90,7 +94,7 @@ public class StoragePathManager
@Override
public void onReceive(Context context, Intent intent)
{
updateExternalStorages();
updateExternalStorages(mActivity.getApplication());
if (mStoragesChangedListener != null)
mStoragesChangedListener.onStorageListChanged(mItems, mCurrentStorageIndex);
@ -98,7 +102,7 @@ public class StoragePathManager
};
mActivity.registerReceiver(mInternalReceiver, getMediaChangesIntentFilter());
updateExternalStorages();
updateExternalStorages(mActivity.getApplication());
}
private static IntentFilter getMediaChangesIntentFilter()
@ -143,57 +147,94 @@ public class StoragePathManager
return mCurrentStorageIndex;
}
private void updateExternalStorages()
private void updateExternalStorages(Application application)
{
updateExternalStorages(StorageUtils.getWritableDirRoot());
}
List<File> candidates = new ArrayList<>();
private void updateExternalStorages(String writableDir)
{
Set<String> pathsFromConfig = new HashSet<>();
StorageUtils.parseStorages(pathsFromConfig);
mItems.clear();
final StorageItem currentStorage = buildStorageItem(writableDir);
addStorageItem(currentStorage);
addStorageItem(buildStorageItem(Environment.getExternalStorageDirectory().getAbsolutePath()));
for (String path : pathsFromConfig)
addStorageItem(buildStorageItem(path));
mCurrentStorageIndex = mItems.indexOf(currentStorage);
if (mCurrentStorageIndex == -1)
// External storages (SD cards and other).
for (File dir : application.getExternalFilesDirs(null))
{
LOGGER.w(TAG, "Unrecognized current path : " + currentStorage);
LOGGER.w(TAG, "Parsed paths : ");
//
// If the contents of emulated storage devices are backed by a private user data partition,
// then there is little benefit to apps storing data here instead of the private directories
// returned by Context#getFilesDir(), etc.
//
if (!Environment.isExternalStorageEmulated(dir))
candidates.add(dir);
}
// Internal storage (always exists).
candidates.add(application.getFilesDir());
// Configured path.
String configDir = Config.getStoragePath();
if (!TextUtils.isEmpty(configDir))
candidates.add(new File(configDir));
// Current path.
String currentDir = Framework.nativeGetWritableDir();
if (!TextUtils.isEmpty(currentDir))
candidates.add(new File(configDir));;
if (candidates.isEmpty())
throw new AssertionError("Can't find available storage");
//
// Update internal state.
//
mItems.clear();
mCurrentStorageIndex = -1;
Set<String> unique = new HashSet<>();
for (File dir : candidates)
{
StorageItem item = buildStorageItem(dir);
if (item != null)
{
String path = item.getFullPath();
if (!unique.add(path))
{
// A duplicate
LOGGER.d(TAG, "Skip a duplicate : " + path);
continue;
}
LOGGER.i(TAG, "Storage found : " + path + ", size : " + item.getFreeSize());
if (!TextUtils.isEmpty(configDir) && configDir.equals(path))
{
mCurrentStorageIndex = mItems.size();
}
mItems.add(item);
}
}
if (!TextUtils.isEmpty(configDir) && mCurrentStorageIndex == -1)
{
LOGGER.w(TAG, "Unrecognized current path : " + configDir);
for (StorageItem item : mItems)
LOGGER.w(TAG, item.toString());
}
}
private void addStorageItem(StorageItem item)
{
if (item != null && !mItems.contains(item))
mItems.add(item);
}
private static StorageItem buildStorageItem(String path)
private static StorageItem buildStorageItem(File dir)
{
String path = dir.getAbsolutePath();
LOGGER.d(TAG, "Check storage : " + path);
try
{
final File f = new File(path + "/");
if (f.exists() && f.isDirectory() && f.canWrite() && StorageUtils.isDirWritable(path))
path = dir.getCanonicalPath();
// Add the trailing separator because the native code assumes that all paths have it.
if (!path.endsWith(File.separator))
path = path + File.separator;
if (dir.exists() && dir.isDirectory() && dir.canWrite() && StorageUtils.isDirWritable(path))
{
final long freeSize = StorageUtils.getFreeBytesAtPath(path);
if (freeSize > 0)
{
LOGGER.i(TAG, "Storage found : " + path + ", size : " + freeSize);
return new StorageItem(path, freeSize);
}
}
} catch (final IllegalArgumentException ex)
}
catch (IllegalArgumentException | IOException ex)
{
LOGGER.e(TAG, "Can't build storage for path : " + path, ex);
}
@ -227,7 +268,7 @@ public class StoragePathManager
@Override
public void moveFilesFinished(String newPath)
{
updateExternalStorages();
updateExternalStorages(mActivity.getApplication());
if (mMoveFilesListener != null)
mMoveFilesListener.moveFilesFinished(newPath);
}
@ -235,7 +276,7 @@ public class StoragePathManager
@Override
public void moveFilesFailed(int errorCode)
{
updateExternalStorages();
updateExternalStorages(mActivity.getApplication());
if (mMoveFilesListener != null)
mMoveFilesListener.moveFilesFailed(errorCode);
}
@ -291,18 +332,27 @@ public class StoragePathManager
candidates[0].list().length > 0);
}
public String findMapsMeStorage(String settingsPath)
public String findMapsStorage(@NonNull Application application)
{
updateExternalStorages(settingsPath);
updateExternalStorages(application);
List<StorageItem> items = getStorageItems();
for (StorageItem item : items)
{
LOGGER.d(TAG, "Scanning: " + item.mPath);
if (containsMapData(item.mPath))
{
LOGGER.i(TAG, "Found map at: " + item.mPath);
return item.mPath;
}
}
return settingsPath;
// Use the first item by default.
final String defaultDir = items.get(0).mPath;
LOGGER.i(TAG, "Using default directory: " + defaultDir);
Config.setStoragePath(defaultDir);
return defaultDir;
}
private void setStoragePath(@NonNull final Activity context,
@ -333,7 +383,7 @@ public class StoragePathManager
else
listener.moveFilesFailed(result);
updateExternalStorages();
updateExternalStorages(mActivity.getApplication());
}
});
}

View file

@ -30,8 +30,6 @@ final class StorageUtils
private static final Logger LOGGER = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.STORAGE);
private static final String TAG = StorageUtils.class.getSimpleName();
private static final int VOLD_MODE = 1;
private static final int MOUNTS_MODE = 2;
/**
* Check if directory is writable. On some devices with KitKat (eg, Samsung S4) simple File.canWrite() returns
@ -53,20 +51,6 @@ final class StorageUtils
return true;
}
/**
* Returns path, where maps and other files are stored.
* @return pat (or empty string, if framework wasn't created yet)
*/
static String getWritableDirRoot()
{
String writableDir = Framework.nativeGetWritableDir();
int index = writableDir.lastIndexOf(Constants.MWM_DIR_POSTFIX);
if (index != -1)
writableDir = writableDir.substring(0, index);
return writableDir;
}
static long getFreeBytesAtPath(String path)
{
long size = 0;
@ -81,68 +65,6 @@ final class StorageUtils
return size;
}
// http://stackoverflow.com/questions/8151779/find-sd-card-volume-label-on-android
// http://stackoverflow.com/questions/5694933/find-an-external-sd-card-location
// http://stackoverflow.com/questions/14212969/file-canwrite-returns-false-on-some-devices-although-write-external-storage-pe
private static void parseMountFile(String file, int mode, Set<String> paths)
{
LOGGER.i(StoragePathManager.TAG, "Parsing " + file);
BufferedReader reader = null;
try
{
reader = new BufferedReader(new FileReader(file));
while (true)
{
String line = reader.readLine();
if (line == null)
return;
line = line.trim();
if (TextUtils.isEmpty(line) || line.startsWith("#"))
continue;
// standard regexp for all possible whitespaces (space, tab, etc)
String[] parts = line.split("\\s+");
if (parts.length <= 3)
continue;
if (mode == VOLD_MODE)
{
if (parts[0].startsWith("dev_mount"))
paths.add(parts[2]);
continue;
}
for (String s : new String[] { "/dev/block/vold", "/dev/fuse", "/mnt/media_rw" })
{
if (parts[0].startsWith(s))
{
paths.add(parts[1]);
break;
}
}
}
} catch (final IOException e)
{
LOGGER.w(TAG, "Can't read file: " + file, e);
} finally
{
Utils.closeSafely(reader);
}
}
static void parseStorages(Set<String> paths)
{
parseMountFile("/etc/vold.conf", VOLD_MODE, paths);
parseMountFile("/etc/vold.fstab", VOLD_MODE, paths);
parseMountFile("/system/etc/vold.fstab", VOLD_MODE, paths);
parseMountFile("/proc/mounts", MOUNTS_MODE, paths);
}
static void copyFile(File source, File dest) throws IOException
{
int maxChunkSize = 10 * Constants.MB; // move file by smaller chunks to avoid OOM.

View file

@ -4,8 +4,6 @@ import com.mapswithme.maps.BuildConfig;
public final class Constants
{
public static final String STORAGE_PATH = "/Android/data/%s/%s/";
public static final int KB = 1024;
public static final int MB = 1024 * 1024;
public static final int GB = 1024 * 1024 * 1024;
@ -68,10 +66,5 @@ public final class Constants
private Rating() {}
}
public static final String MWM_DIR_POSTFIX = "/OMapsData/";
public static final String CACHE_DIR = "cache";
public static final String FILES_DIR = "files";
private Constants() {}
}

View file

@ -109,49 +109,33 @@ public class StorageUtils
}
@NonNull
public static String getSettingsPath()
private static String addTrailingSeparator(@NonNull String dir)
{
return Environment.getExternalStorageDirectory().getAbsolutePath() + Constants.MWM_DIR_POSTFIX;
if (!dir.endsWith("/"))
return dir + File.separator;
return dir;
}
@NonNull
public static String getStoragePath(@NonNull String settingsPath)
public static String getSettingsPath(@NonNull Application application)
{
String path = Config.getStoragePath();
if (!TextUtils.isEmpty(path))
{
File f = new File(path);
if (f.exists() && f.isDirectory())
return path;
path = new StoragePathManager().findMapsMeStorage(settingsPath);
Config.setStoragePath(path);
return path;
}
return settingsPath;
return addTrailingSeparator(application.getFilesDir().getAbsolutePath());
}
@NonNull
public static String getFilesPath(@NonNull Application application)
public static String getPrivatePath(@NonNull Application application)
{
final File filesDir = application.getExternalFilesDir(null);
if (filesDir != null)
return filesDir.getAbsolutePath();
return Environment.getExternalStorageDirectory().getAbsolutePath() +
String.format(Constants.STORAGE_PATH, BuildConfig.APPLICATION_ID, Constants.FILES_DIR);
return addTrailingSeparator(application.getFilesDir().getAbsolutePath());
}
@NonNull
public static String getTempPath(@NonNull Application application)
{
final File cacheDir = application.getExternalCacheDir();
final File cacheDir = application.getExternalCacheDir();
if (cacheDir != null)
return cacheDir.getAbsolutePath();
return addTrailingSeparator(cacheDir.getAbsolutePath());
return Environment.getExternalStorageDirectory().getAbsolutePath() +
String.format(Constants.STORAGE_PATH, BuildConfig.APPLICATION_ID, Constants.CACHE_DIR);
return addTrailingSeparator(application.getCacheDir().getAbsolutePath());
}
public static boolean createDirectory(@NonNull Context context, @NonNull String path)
@ -159,13 +143,8 @@ public class StorageUtils
File directory = new File(path);
if (!directory.exists() && !directory.mkdirs())
{
boolean isPermissionGranted = PermissionsUtils.isExternalStorageGranted(context);
Throwable error = new IllegalStateException("Can't create directories for: " + path
+ " state = " + Environment.getExternalStorageState()
+ " isPermissionGranted = " + isPermissionGranted);
LOGGER.e(TAG, "Can't create directories for: " + path
+ " state = " + Environment.getExternalStorageState()
+ " isPermissionGranted = " + isPermissionGranted);
Throwable error = new IllegalStateException("Can't create directories for: " + path);
LOGGER.e(TAG, "Can't create directories for: " + path);
CrashlyticsUtils.INSTANCE.logException(error);
return false;
}