Android SAF External File Control Extension
안드로이드의 SAF를 이용한 폴더 및 파일 접근
(2022. 06. 22. 업데이트) 익스텐션 v1.1.4 한국어 문서입니다.
목차
소개
기존에 파일 쓰기 및 읽기 권한만을 가지고 있다면 외부 저장소의 파일 접근이 쉽게 됐던 것과 달리, 안드로이드 API 30 (Android 11) 이후부터는 SAF(Storage Access Framework)를 이용해 접근 범위를 사용자가 지정해야 파일 접근을 할 수 있게 되었습니다. 때문에 권한이 있더라도 외부 저장소에서 게임메이커 내장 함수를 사용하다 보면 대부분의 함수가 작동하지 않습니다. 해당 익스텐션은 SAF를 이용하여 사용자가 원하는 경로를 선택하면, 콘텐츠 기반으로 해당 경로 및 하위 경로의 파일들에 접근할 수 있도록 도와줍니다.
주의사항
URI(Uniform Resource Identifiers)는 각 콘텐츠(파일)에 대한 고유한 식별자입니다. 하위 개념으로 익히 들어봤을 법한 URL, 그렇지 않을 URN이 있습니다. 여기서 중요한 것은 URL입니다. URL은 위치를 기반으로 파일을 식별합니다. 즉, 파일의 경로가 식별자가 됩니다. URL 앞부분의 https://, file://, content:// 등과 같은 스키마가 이 파일이 어떤 방식으로 사용될지를 구분합니다. SAF를 이용한 파일 접근은 기존에 사용하던 file://filePath URL 대신, content://contentID URL을 사용합니다.
그러나 이 익스텐션에서는 익숙한 기존의 파일 URL 방식을 FILE(file://filePath URL) 유형으로, 콘텐츠 URL 방식을 SAF(content://contentID) 유형으로 정의했으며, 문서에서도 이를 기준으로 설명합니다. 그 편이 함수 이름 길이를 줄이는데 도움이 되고, 안드로이드에 익숙하지 않은 개발자도 접근하기 쉬울 것이기 때문입니다. 때문에, SAF 공간에 접근해야 하는 함수에 대해 saf_ 접두사가 붙습니다.
요구사항
게임메이커 스튜디오 2 2.3.0 (아마도 하위 버전을 지원하지 않을 겁니다.)
안드로이드 API 24 (Nougat) 이상의 기기만을 지원합니다.
- SAF (API 19, Kitkat)
- RealPathUtil (API 23, Mashmallow)
- DocumentsContract (API 24, Nougat)
사용 가능한 함수
Intent 함수
String intent_saf_request(double request_code)
사용자가 원하는 경로를 설정할 수 있도록 SAF 선택 화면을 띄우는 함수입니다. request_code는 다음의 두 가지가 있습니다.
- SAF_REQUEST_SEARCH
- SAF_REQUEST_LOAD
SAF_REQUEST_SEARCH: 이 요청 코드를 통해 앱이 사용할 최상위 경로(root)를 사용자가 선택할 수 있게 합니다. 설정한 경로는 내부 설정에 저장됩니다. 함수가 빈 문자열("")을 반환하지만, Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "saf_request_accepted", "saf_request_canceled"
"path": Absolute Path
type - saf_request_accepted는 사용자가 성공적으로 경로를 지정했음을 알립니다.
type - saf_request_canceled는 사용자가 SAF 인텐트를 종료했음을 알립니다. 이 경우 path는 undefined입니다.
SAF_REQUEST_LOAD: 이 요청 코드를 통해 내부 설정에 저장된 최상위 경로를 반환합니다. 저장된 경로가 없다면 빈 문자열("")을 반환합니다. 저장된 경로가 있다면 저장된 문자열을 반환하며 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "saf_request_loaded"
"path": Absolute Path
type - saf_request_loaded는 저장한 경로가 존재함을 알립니다.
void intent_open_setting(String message)
앱 설정 화면을 엽니다. 메시지 매개변수가 빈 문자열("")이 아니라면 토스트 메시지를 띄우면서 실행됩니다. 함수가 아무것도 반환하지 않지만 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "permission_check"
이를 이용하여 앱 설정 이후 권한이 제대로 설정되었는지 체크해 볼 수 있습니다.
FILE 유형 함수
String directory_get_external_files() String directory_get_external_cache()
/storage/emulated/0/Android/AppPackage (경로는 기기마다 다를 수 있음)에 접근하는 건 권한이 필요 없으며 경로만 얻을 수 있다면 게임메이커 내의 함수로도 파일을 관리할 수 있습니다. 해당 경로를 얻기 위해서는 위의 두 함수를 사용할 수 있습니다.
각각 /storage/emulated/0/Android/AppPackage/ 내의 files, cache 폴더의 경로를 반환하며, 두 경로는 특수한 권한이 없이 파일 읽고 쓰기가 가능하며, 게임메이커 내의 파일 함수를 이용하여 파일 관리도 가능합니다. 그러나 패키지 폴더 내의 파일은 앱 제거 시 같이 제거되며, 이는 게임 내의 중요한 파일(저장 데이터 또는 사용자 배포 파일)의 손실로 이어질 수 있습니다.
저장소에 대한 자세한 내용은 다음을 참조하세요.
https://developer.android.com/training/data-storage?hl=ko
String directory_get_saf_root()
intent_saf_request를 이용하여 설정한 SAF 최상위 경로를 FILE 유형 절대 경로로 반환합니다. SAF 최상위 경로가 설정되지 않았다면 빈 문자열("")을 반환합니다.
String directory_get_contents(String path)
디렉터리에 어떤 파일이 존재하는지 확인할 수 있습니다. json 형식의 텍스트가 반환되며, json_decode 함수를 이용하여 ds_map으로 변환할 수 있습니다. 키 값은 다음과 같습니다.
"directory_length": 디렉터리 개수
"file_length": 파일 개수
"directory_N": N번째 디렉터리의 이름 (N = 0 ~ "directory_length" - 1)
"file_N": N번째 파일의 이름 (N = 0 ~ "file_length" - 1)
double file_get_size(String path, String name)
파일의 크기를 확인합니다. SAF 유형 파일이더라도 권한과 절대 경로가 있으면 사용할 수 있습니다.
String file_apply_name(String path, String name)
파일의 이름이 유효한지 확인합니다. SAF 유형 파일이더라도 권한과 절대 경로가 있으면 사용할 수 있습니다.파일의 이름에 유효하지 않은 기호( | \ ? * < " : > / )가 있다면 언더바( _ )로 바뀝니다.
또한 해당 경로에 이름이 동일한 파일이 있다면 옆에 추가적으로 인덱스 번호가 붙습니다.
SAF 유형 함수
SAF 유형 함수를 사용하려면 사용자가 앱이 파일에 접근하는 것을 동의해야 합니다.
이 익스텐션에서는 구현하지 않으며, 개발자가 직접 os_check_permission, os_request_permission 두 함수를 이용해 권한을 획득해야 합니다.
SAF 유형에서 경로(path)는 사용자가 설정한 경로를 최상위 경로(루트, "/")로 합니다. 항상 슬래시( / )로 구분해야 하며 역슬래시, 포함할 수 없는 기호 등을 입력하면 대신 언더 바가 삽입되거나 하는 등, 예상한 대로 작동하지 않게 됩니다. 이름(name) 역시 마찬가지로 포함할 수 없는 기호는 언더바( _ )로 변경됩니다. 중복되는 파일이 있다면 자동으로 이름에 번호를 붙입니다. 또한 디렉터리 경로는 하나로 합쳐 써야 하며, 파일 경로는 디렉터리 경로와 파일 경로를 분리해서 써야 합니다.
항상 SAF 범위 내에서만 사용될 수 있습니다.
double saf_directory_create(String path, String name)
입력한 경로에서 이름에 해당하는 디렉터리를 생성합니다. 디렉터리가 존재하지 않거나 이미 같은 이름의 디렉터리가 있다면 실패합니다. 성공하면 1, 실패하면 0을 반환합니다.
void saf_directory_creates(String path)
경로까지의 디렉터리를 생성합니다. 디렉터리가 없다면 디렉터리를 만들어냅니다. (mkdir와 같습니다.)
double saf_directory_exists(String path)
디렉터리가 존재하는지 확인합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_directory_rename(String path, String rename)
디렉터리의 이름을 변경합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_directory_remove(String path)
디렉터리를 제거합니다. 성공하면 1, 실패하면 0을 반환합니다.
String saf_directory_get_contents(String path)
디렉터리에 어떤 파일이 존재하는지 확인할 수 있습니다. json 형식의 텍스트가 반환되며, json_decode 함수를 이용하여 ds_map으로 변환할 수 있습니다. 키 값은 다음과 같습니다.
"directory_length": 디렉터리 개수
"file_length": 파일 개수
"directory_N": N번째 디렉터리의 이름 (N = 0 ~ "directory_length" - 1)
"file_N": N번째 파일의 이름 (N = 0 ~ "file_length" - 1)
double saf_file_create_text(String path, String name)
경로에 텍스트 파일(MIME Type: "text/plain")을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_file_create_bin(String path, String name)
경로에 바이너리 파일(MIME Type: "application/octet-stream")을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_file_create(String path, String name, String mime_type)
경로에 MIME Type에 해당하는 파일을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.
MIME Type에 대해서는 다음을 참고하세요.
https://www.iana.org/assignments/media-types/media-types.xhtml
double saf_file_exists(String path, String name)
파일이 존재하는지 확인합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_file_rename(String path, String name, String rename)
파일의 이름을 변경합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_file_remove(String path, String name)
파일을 제거합니다. 성공하면 1, 실패하면 0을 반환합니다.
double saf_file_move(String src_path, String src_name, String dst_path, String dst_name)
파일을 옮깁니다. 성공하면 1, 실패하면 0을 반환합니다. (SAF to SAF)
동일한 이름을 가진 파일이 있다면 이름이 임의로 변경되므로 move 대신 copy after delete 방식으로 파일을 이동하는 걸 추천합니다.
void saf_file_copy(String path_src, String name_src, String path_dst, name_dst)
src 파일을 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (SAF to SAF)
void saf_file_copy_from_file(String path_src, String name_src, String path_dst, name_dst)
FILE 유형 src 파일을 SAF 유형 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (FILE to SAF)
void saf_file_copy_to_file(String path_src, String name_src, String path_dst, name_dst)
SAF 유형 src 파일을 FILE 유형 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (SAF to FILE)
SAF IOStream 함수
게임메이커 스튜디오 2의 파일 스트림을 요구하는 함수 (file_text_open_read, file_bin_open, ini_open 등) 들은, SAF 공간에서 사용할 수 없습니다. 때문에 익스텐션에서 따로 구현되었습니다. (기존 함수와 기능 차이가 있습니다.)
동일한 파일을 여러 모드로 열 수 없도록 막혀있습니다. 또한 한 파일을 텍스트 파일과 바이너리 파일로 동시에 열어서는 안 됩니다.
double saf_file_text_open_read(String path, String name)
텍스트 파일을 읽기 모드로 엽니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
double saf_file_text_open_write(String path, String name)
텍스트 파일을 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
double saf_file_text_open_append(String path, String name)
텍스트 파일을 이어 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
double saf_file_text_read_real(double file)
텍스트 파일에서 실수 하나를 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.
String saf_file_text_read_string(double file)
텍스트 파일에서 문자열 하나를 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.
String saf_file_text_readln(double file)
텍스트 파일에서 한 줄을 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.
void saf_file_text_write_real(double file, double value)
텍스트 파일에 실수를 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.
void saf_file_text_write_string(double file, String value)
텍스트 파일에 문자열을 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.
void saf_file_text_writeln(double file, String value)
텍스트 파일에 문자열을 쓴 후에 줄을 바꿉니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.
double saf_file_text_eoln(double file)
텍스트 파일에 다음 줄이 있는지 확인합니다. 존재하면 1, 그렇지 않다면 0을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.
double saf_file_text_eof(double file)
텍스트 파일이 끝났는지 확인합니다. 끝이라면 1, 아니라면 0을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.
void saf_file_text_close(double file)
텍스트 파일을 닫습니다.
double saf_file_bin_open_read(String path, String name)
바이너리 파일을 읽기 모드로 엽니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
double saf_file_bin_open_write(String path, String name)
바이너리 파일을 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
double saf_file_bin_open_append(String path, String name)
바이너리 파일을 이어 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.
void saf_file_bin_rewrite(double file)
바이너리 파일을 비웁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.
void saf_file_bin_write_byte(double file, double value)
바이너리 파일에 바이트 하나를 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.
double saf_file_bin_read_byte(double file)
바이너리 파일에서 바이트 하나를 읽습니다. 파일의 끝이라면 -1을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.
void saf_file_bin_close(double file)
바이너리 파일을 닫습니다.
디버그 함수
디버그 용도로 사용할 수 있는 함수입니다. ExternalFile.java 내부에 작성되어 있습니다.
double send_double(double value)
입력한 실수를 반환합니다. 이 함수(ExternalFile.java: 671)를 수정하여 테스트 용도로 사용할 수 있습니다. 인자 갯수, 타입 및 반환 타입을 수정하려면 게임메이커 내의 익스텐션 함수 연결 부분도 같이 수정해야 합니다.
void send_social_log(String text)
함수가 아무것도 반환하지 않지만 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "log"
"log": text
통합 함수
SAF 공간에서 게임메이커 일부 내장 함수를 사용할 수 있으므로 의도가 동일하지만 함수 형태가 다른 함수를 통합하기 위해 통합 함수를 만들었습니다. 통합 함수는 ExternalFile.gml 내부에 작성되어 있으며 integrated_ 접두사가 붙습니다.
String integrated_file_path(path, name, type)
FILE 유형의 경로(절대 경로)와 SAF 유형의 경로(상대 경로)에서 절대 경로를 반환합니다. type 은 두 가지가 있습니다.
- IS_FILE
- IS_SAF
IS_FILE 일 때는 path + "/" + name 인 절대 경로를 반환합니다.
IS_SAF 일 때는 설정된 SAF 루트의 절대 경로를 불러와 path + "/" + name에 더한 경로를 반환합니다. SAF 공간이 설정되어 있지 않다면 의도한 대로 작동하지 않을 것입니다.
void integrated_file_copy(path_src, name_src, path_dst, name_dst, type_src, type_dst)
src 파일의 유형과 dst 파일의 유형에 따라 각각 다른 함수를 사용하여 파일을 복사합니다. type 은 두 가지가 있습니다.
- IS_FILE
- IS_SAF
참고사항
게임메이커 내장 함수는 앱 별 내부 저장소(/data/data/AppPackage) 및 외부 저장소에서(/storage/emulated/0/Android/AppPackage)도 사용할 수 있습니다. 그 중 일부 함수는 SAF 공간 내부에서도 작동합니다. 이 경우, SAF 유형의 경로(상대 경로) 대신 FILE 유형의 경로(절대 경로)가 필요합니다.
- file_copy (SAF 공간 내부의 파일을 앱 별 저장소로 복사할 수 있습니다. 이 경우 확장자 관련 오류가 있으니 saf_file_copy_to_file을 이용하시는 것을 권장합니다.)
- font_add
- audio_create_stream
- sprite_add
파일을 메모리에 올려두는 함수에 한에서만 작동하는 것을 알 수 있습니다. (이마저도 읽기는 성공할 수 있으나 쓰기는 권한이 있더라도 항상 실패합니다.)
익스텐션에 포함된 RealPathUtil.java는 아래 사이트의 코드를 가져와 하위 호환성을 제거했습니다.
https://gist.github.com/tatocaster/32aad15f6e0c50311626
'작업실 > 도구' 카테고리의 다른 글
SFGenerator.exe: 스프라이트 폰트 생성기 (0) | 2021.12.23 |
---|---|
Merge2CSV.py: CSV 병합 도구 (0) | 2021.09.13 |
AutoReplace.py: 표현식 기반 탐색 후 변환 도구 (0) | 2021.09.13 |
댓글