Android: Allow opening/getting files relative to downloads directory

This commit is contained in:
Connor McLaughlin 2021-04-24 16:03:28 +10:00
parent 600ae7bcc0
commit 46d19eeb1f
16 changed files with 281 additions and 41 deletions

View File

@ -20,6 +20,7 @@ import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* File helper class - used to bridge native code to Java storage access framework APIs.
@ -53,6 +54,21 @@ public class FileHelper {
DocumentsContract.Document.COLUMN_LAST_MODIFIED
};
/**
* Projection used when getting the display name for a file.
*/
private static final String[] getDisplayNameProjection = new String[]{
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
};
/**
* Projection used when getting a relative file for a file.
*/
private static final String[] getRelativeFileProjection = new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
};
private final Context context;
private final ContentResolver contentResolver;
@ -244,6 +260,7 @@ public class FileHelper {
/**
* Returns the file name component of a path or URI.
*
* @param path Path/URI to examine.
* @return File name component of path/URI.
*/
@ -265,6 +282,14 @@ public class FileHelper {
return path;
}
/**
* Test if the given URI represents a {@link DocumentsContract.Document} tree.
*/
public static boolean isTreeUri(Uri uri) {
final List<String> paths = uri.getPathSegments();
return (paths.size() >= 2 && paths.get(0).equals("tree"));
}
/**
* Retrieves a file descriptor for a content URI string. Called by native code.
*
@ -353,6 +378,91 @@ public class FileHelper {
}
}
/**
* Returns the display name for the given URI.
*
* @param uriString URI to resolve display name for.
* @return display name for the URI, or null.
*/
public String getDisplayNameForURIPath(String uriString) {
try {
final Uri fullUri = Uri.parse(uriString);
final Cursor cursor = contentResolver.query(fullUri, getDisplayNameProjection,
null, null, null);
if (cursor.getCount() == 0 || !cursor.moveToNext())
return null;
return cursor.getString(0);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Returns the path for a sibling file relative to another URI.
*
* @param uriString URI to find the file relative to.
* @param newFileName Sibling file name.
* @return URI for the sibling file name, or null.
*/
public String getRelativePathForURIPath(String uriString, String newFileName) {
try {
final Uri fullUri = Uri.parse(uriString);
// if this is a document (expected)...
Uri treeUri;
String treeDocId;
if (DocumentsContract.isDocumentUri(context, fullUri)) {
// we need to remove the last part of the URI (the specific document ID) to get the parent
final String lastPathSegment = fullUri.getLastPathSegment();
int lastSeparatorIndex = lastPathSegment.lastIndexOf('/');
if (lastSeparatorIndex < 0)
lastSeparatorIndex = lastPathSegment.lastIndexOf(':');
if (lastSeparatorIndex < 0)
return null;
// the parent becomes the document ID
treeDocId = lastPathSegment.substring(0, lastSeparatorIndex);
// but, we need to access it through the subtree if this was a tree URI (permissions...)
if (isTreeUri(fullUri)) {
treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), DocumentsContract.getTreeDocumentId(fullUri));
} else {
treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), treeDocId);
}
} else {
treeDocId = DocumentsContract.getDocumentId(fullUri);
treeUri = fullUri;
}
final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, treeDocId);
final Cursor cursor = contentResolver.query(queryUri, getRelativeFileProjection, null, null, null);
final int count = cursor.getCount();
while (cursor.moveToNext()) {
try {
final String displayName = cursor.getString(1);
if (!displayName.equalsIgnoreCase(newFileName))
continue;
final String childDocumentId = cursor.getString(0);
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId);
cursor.close();
return uri.toString();
} catch (Exception e) {
e.printStackTrace();
}
}
cursor.close();
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Java class containing the data for a file in a find operation.
*/

View File

@ -2,6 +2,7 @@
#include "assert.h"
#include "file_system.h"
#include "log.h"
#include "string_util.h"
#include <array>
Log_SetChannel(CDImage);
@ -17,45 +18,50 @@ u32 CDImage::GetBytesPerSector(TrackMode mode)
std::unique_ptr<CDImage> CDImage::Open(const char* filename, Common::Error* error)
{
const char* extension = std::strrchr(filename, '.');
const char* extension;
#ifdef __ANDROID__
std::string filename_display_name(FileSystem::GetDisplayNameFromPath(filename));
if (filename_display_name.empty())
filename_display_name = filename;
extension = std::strrchr(filename_display_name.c_str(), '.');
#else
extension = std::strrchr(filename, '.');
#endif
if (!extension)
{
Log_ErrorPrintf("Invalid filename: '%s'", filename);
return nullptr;
}
#ifdef _MSC_VER
#define CASE_COMPARE _stricmp
#else
#define CASE_COMPARE strcasecmp
#endif
if (CASE_COMPARE(extension, ".cue") == 0)
if (StringUtil::Strcasecmp(extension, ".cue") == 0)
{
return OpenCueSheetImage(filename, error);
}
else if (CASE_COMPARE(extension, ".bin") == 0 || CASE_COMPARE(extension, ".img") == 0 ||
CASE_COMPARE(extension, ".iso") == 0)
else if (StringUtil::Strcasecmp(extension, ".bin") == 0 || StringUtil::Strcasecmp(extension, ".img") == 0 ||
StringUtil::Strcasecmp(extension, ".iso") == 0)
{
return OpenBinImage(filename, error);
}
else if (CASE_COMPARE(extension, ".chd") == 0)
else if (StringUtil::Strcasecmp(extension, ".chd") == 0)
{
return OpenCHDImage(filename, error);
}
else if (CASE_COMPARE(extension, ".ecm") == 0)
else if (StringUtil::Strcasecmp(extension, ".ecm") == 0)
{
return OpenEcmImage(filename, error);
}
else if (CASE_COMPARE(extension, ".mds") == 0)
else if (StringUtil::Strcasecmp(extension, ".mds") == 0)
{
return OpenMdsImage(filename, error);
}
else if (CASE_COMPARE(extension, ".pbp") == 0)
else if (StringUtil::Strcasecmp(extension, ".pbp") == 0)
{
return OpenPBPImage(filename, error);
}
else if (CASE_COMPARE(extension, ".m3u") == 0)
else if (StringUtil::Strcasecmp(extension, ".m3u") == 0)
{
return OpenM3uImage(filename, error);
}

View File

@ -94,7 +94,7 @@ bool CDImageBin::Open(const char* filename, Common::Error* error)
AddLeadOutIndex();
m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str());
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}

View File

@ -279,7 +279,7 @@ bool CDImageCHD::Open(const char* filename, Common::Error* error)
m_lba_count = disc_lba;
AddLeadOutIndex();
m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str());
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}

View File

@ -269,7 +269,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error)
m_lba_count = disc_lba;
AddLeadOutIndex();
m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str());
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}

View File

@ -385,7 +385,7 @@ bool CDImageEcm::Open(const char* filename, Common::Error* error)
AddLeadOutIndex();
m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str());
m_sbi.LoadSBIFromImagePath(filename);
m_chunk_buffer.reserve(RAW_SECTOR_SIZE * 2);
return Seek(1, Position{0, 0, 0});

View File

@ -250,7 +250,7 @@ bool CDImageMds::OpenAndParse(const char* filename, Common::Error* error)
m_lba_count = m_tracks.back().start_lba + m_tracks.back().length;
AddLeadOutIndex();
m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str());
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}

View File

@ -673,8 +673,8 @@ bool CDImagePBP::OpenDisc(u32 index, Common::Error* error)
if (m_disc_offsets.size() > 1)
{
std::string sbi_path =
FileSystem::StripExtension(m_filename) + StringUtil::StdStringFromFormat("_%u.sbi", index + 1);
std::string sbi_path(FileSystem::StripExtension(m_filename));
sbi_path += TinyString::FromFormat("_%u.sbi", index + 1);
m_sbi.LoadSBI(sbi_path.c_str());
}
else

View File

@ -83,6 +83,11 @@ bool CDSubChannelReplacement::LoadSBI(const char* path)
return true;
}
bool CDSubChannelReplacement::LoadSBIFromImagePath(const char* image_path)
{
return LoadSBI(FileSystem::ReplaceExtension(image_path, "sbi").c_str());
}
void CDSubChannelReplacement::AddReplacementSubChannelQ(u32 lba, const CDImage::SubChannelQ& subq)
{
auto iter = m_replacement_subq.find(lba);

View File

@ -14,6 +14,7 @@ public:
u32 GetReplacementSectorCount() const { return static_cast<u32>(m_replacement_subq.size()); }
bool LoadSBI(const char* path);
bool LoadSBIFromImagePath(const char* image_path);
/// Adds a sector to the replacement map.
void AddReplacementSubChannelQ(u32 lba, const CDImage::SubChannelQ& subq);

View File

@ -51,6 +51,8 @@ static jfieldID s_android_FileHelper_FindResult_relativeName;
static jfieldID s_android_FileHelper_FindResult_size;
static jfieldID s_android_FileHelper_FindResult_modifiedTime;
static jfieldID s_android_FileHelper_FindResult_flags;
static jmethodID s_android_FileHelper_getDisplayName;
static jmethodID s_android_FileHelper_getRelativePathForURIPath;
// helper for retrieving the current per-thread jni environment
static JNIEnv* GetJNIEnv()
@ -89,6 +91,8 @@ void SetAndroidFileHelper(void* jvm, void* env, void* object)
jenv->DeleteGlobalRef(s_android_FileHelper_object);
jenv->DeleteGlobalRef(s_android_FileHelper_class);
s_android_FileHelper_getRelativePathForURIPath = {};
s_android_FileHelper_getDisplayName = {};
s_android_FileHelper_openURIAsFileDescriptor = {};
s_android_FileHelper_FindFiles = {};
s_android_FileHelper_object = {};
@ -114,7 +118,13 @@ void SetAndroidFileHelper(void* jvm, void* env, void* object)
s_android_FileHelper_FindFiles =
jenv->GetMethodID(s_android_FileHelper_class, "findFiles",
"(Ljava/lang/String;I)[Lcom/github/stenzek/duckstation/FileHelper$FindResult;");
Assert(s_android_FileHelper_openURIAsFileDescriptor && s_android_FileHelper_FindFiles);
s_android_FileHelper_getDisplayName =
jenv->GetMethodID(s_android_FileHelper_class, "getDisplayNameForURIPath", "(Ljava/lang/String;)Ljava/lang/String;");
s_android_FileHelper_getRelativePathForURIPath =
jenv->GetMethodID(s_android_FileHelper_class, "getRelativePathForURIPath",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
Assert(s_android_FileHelper_openURIAsFileDescriptor && s_android_FileHelper_FindFiles &&
s_android_FileHelper_getDisplayName && s_android_FileHelper_getRelativePathForURIPath);
jclass fr_class = jenv->FindClass("com/github/stenzek/duckstation/FileHelper$FindResult");
Assert(fr_class);
@ -279,6 +289,68 @@ static bool FindUriFiles(const char* path, const char* pattern, u32 flags, FindR
return true;
}
static bool GetDisplayNameForUriPath(const char* path, std::string* result)
{
if (!s_android_FileHelper_object)
return false;
JNIEnv* env = GetJNIEnv();
jstring path_jstr = env->NewStringUTF(path);
jstring result_jstr = static_cast<jstring>(
env->CallObjectMethod(s_android_FileHelper_object, s_android_FileHelper_getDisplayName, path_jstr));
env->DeleteLocalRef(path_jstr);
if (!result_jstr)
return false;
const char* result_name = env->GetStringUTFChars(result_jstr, nullptr);
if (result_name)
{
Log_DevPrintf("GetDisplayNameForUriPath(\"%s\") -> \"%s\"", path, result_name);
result->assign(result_name);
}
else
{
result->clear();
}
env->ReleaseStringUTFChars(result_jstr, result_name);
env->DeleteLocalRef(result_jstr);
return true;
}
static bool GetRelativePathForUriPath(const char* path, const char* filename, std::string* result)
{
if (!s_android_FileHelper_object)
return false;
JNIEnv* env = GetJNIEnv();
jstring path_jstr = env->NewStringUTF(path);
jstring filename_jstr = env->NewStringUTF(filename);
jstring result_jstr = static_cast<jstring>(env->CallObjectMethod(
s_android_FileHelper_object, s_android_FileHelper_getRelativePathForURIPath, path_jstr, filename_jstr));
env->DeleteLocalRef(filename_jstr);
env->DeleteLocalRef(path_jstr);
if (!result_jstr)
return false;
const char* result_name = env->GetStringUTFChars(result_jstr, nullptr);
if (result_name)
{
Log_DevPrintf("GetRelativePathForUriPath(\"%s\", \"%s\") -> \"%s\"", path, filename, result_name);
result->assign(result_name);
}
else
{
result->clear();
}
env->ReleaseStringUTFChars(result_jstr, result_name);
env->DeleteLocalRef(result_jstr);
return true;
}
#endif // __ANDROID__
ChangeNotifier::ChangeNotifier(const String& directoryPath, bool recursiveWatch)
@ -510,17 +582,33 @@ bool IsAbsolutePath(const std::string_view& path)
#endif
}
std::string StripExtension(const std::string_view& path)
std::string_view StripExtension(const std::string_view& path)
{
std::string_view::size_type pos = path.rfind('.');
if (pos == std::string::npos)
return std::string(path);
return path;
return std::string(path, 0, pos);
return path.substr(0, pos);
}
std::string ReplaceExtension(const std::string_view& path, const std::string_view& new_extension)
{
#ifdef __ANDROID__
// This is more complex on android because the path may not contain the actual filename.
if (IsUriPath(path))
{
std::string display_name(GetDisplayNameFromPath(path));
std::string_view::size_type pos = display_name.rfind('.');
if (pos == std::string::npos)
return std::string(path);
display_name.erase(pos + 1);
display_name.append(new_extension);
return BuildRelativePath(path, display_name);
}
#endif
std::string_view::size_type pos = path.rfind('.');
if (pos == std::string::npos)
return std::string(path);
@ -572,6 +660,28 @@ static std::string_view::size_type GetLastSeperatorPosition(const std::string_vi
return last_separator;
}
std::string GetDisplayNameFromPath(const std::string_view& path)
{
#if defined(__ANDROID__)
std::string result;
if (IsUriPath(path))
{
std::string temp(path);
if (!GetDisplayNameForUriPath(temp.c_str(), &result))
result = std::move(temp);
}
else
{
result = path;
}
return result;
#else
return std::string(GetFileNameFromPath(path));
#endif
}
std::string_view GetPathDirectory(const std::string_view& path)
{
std::string::size_type pos = GetLastSeperatorPosition(path, false);
@ -583,7 +693,7 @@ std::string_view GetPathDirectory(const std::string_view& path)
std::string_view GetFileNameFromPath(const std::string_view& path)
{
std::string::size_type pos = GetLastSeperatorPosition(path, true);
std::string_view::size_type pos = GetLastSeperatorPosition(path, true);
if (pos == std::string_view::npos)
return path;
@ -630,6 +740,15 @@ std::vector<std::string> GetRootDirectoryList()
std::string BuildRelativePath(const std::string_view& filename, const std::string_view& new_filename)
{
std::string new_string;
#ifdef __ANDROID__
if (IsUriPath(filename) &&
GetRelativePathForUriPath(std::string(filename).c_str(), std::string(new_filename).c_str(), &new_string))
{
return new_string;
}
#endif
std::string_view::size_type pos = GetLastSeperatorPosition(filename, true);
if (pos != std::string_view::npos)
new_string.assign(filename, 0, pos);

View File

@ -145,11 +145,15 @@ void SanitizeFileName(std::string& Destination, bool StripSlashes = true);
bool IsAbsolutePath(const std::string_view& path);
/// Removes the extension of a filename.
std::string StripExtension(const std::string_view& path);
std::string_view StripExtension(const std::string_view& path);
/// Replaces the extension of a filename with another.
std::string ReplaceExtension(const std::string_view& path, const std::string_view& new_extension);
/// Returns the display name of a filename. Usually this is the same as the path, except on Android
/// where it resolves a content URI to its name.
std::string GetDisplayNameFromPath(const std::string_view& path);
/// Returns the directory component of a filename.
std::string_view GetPathDirectory(const std::string_view& path);

View File

@ -33,8 +33,7 @@ HostInterface::HostInterface()
g_host_interface = this;
// we can get the program directory at construction time
const std::string program_path = FileSystem::GetProgramPath();
m_program_directory = FileSystem::GetPathDirectory(program_path.c_str());
m_program_directory = FileSystem::GetPathDirectory(FileSystem::GetProgramPath());
}
HostInterface::~HostInterface()
@ -896,8 +895,7 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings)
void HostInterface::SetUserDirectoryToProgramDirectory()
{
const std::string program_path(FileSystem::GetProgramPath());
const std::string program_directory(FileSystem::GetPathDirectory(program_path.c_str()));
const std::string program_directory(FileSystem::GetProgramPath());
m_user_directory = program_directory;
}

View File

@ -2914,7 +2914,7 @@ void CommonHostInterface::GetGameInfo(const char* path, CDImage* image, std::str
}
else
{
*title = FileSystem::GetFileTitleFromPath(path);
*title = FileSystem::GetFileTitleFromPath(std::string(path));
if (image)
*code = System::GetGameCodeForImage(image, true);
}

View File

@ -662,7 +662,7 @@ static void DoChangeDiscFromFile()
};
OpenFileSelector(ICON_FA_COMPACT_DISC " Select Disc Image", false, std::move(callback), GetDiscImageFilters(),
std::string(FileSystem::GetPathDirectory(System::GetMediaFileName().c_str())));
std::string(FileSystem::GetPathDirectory(System::GetMediaFileName())));
}
static void DoChangeDisc()
@ -1076,7 +1076,7 @@ static bool SettingInfoButton(const SettingInfo& si, const char* section)
CloseFileSelector();
};
OpenFileSelector(si.visible_name, false, std::move(callback), ImGuiFullscreen::FileSelectorFilters(),
std::string(FileSystem::GetPathDirectory(value.c_str())));
std::string(FileSystem::GetPathDirectory(std::move(value))));
}
return false;
@ -2372,7 +2372,7 @@ void DrawQuickMenu(MainWindowType type)
SmallString subtitle;
if (!code.empty())
subtitle.Format("%s - ", code.c_str());
subtitle.AppendString(FileSystem::GetFileNameFromPath(System::GetRunningPath().c_str()));
subtitle.AppendString(FileSystem::GetFileNameFromPath(System::GetRunningPath()));
const ImVec2 title_size(
g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, title.c_str()));
@ -2805,7 +2805,7 @@ void DrawGameListWindow()
else
summary.Format("%s - %s - ", entry->code.c_str(), Settings::GetDiscRegionName(entry->region));
summary.AppendString(FileSystem::GetFileNameFromPath(entry->path.c_str()));
summary.AppendString(FileSystem::GetFileNameFromPath(entry->path));
ImGui::GetWindowDrawList()->AddImage(cover_texture->GetHandle(), bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));

View File

@ -487,11 +487,8 @@ void GameList::ScanDirectory(const char* path, bool recursive, ProgressCallback*
if (AddFileFromCache(ffd.FileName, modified_time))
continue;
const std::string_view file_part(FileSystem::GetFileNameFromPath(ffd.FileName));
if (!file_part.empty())
progress->SetFormattedStatusText("Scanning '%*s'...", static_cast<int>(file_part.size()), file_part.data());
// ownership of fp is transferred
progress->SetFormattedStatusText("Scanning '%s'...", FileSystem::GetDisplayNameFromPath(ffd.FileName).c_str());
ScanFile(std::move(ffd.FileName), modified_time);
}