1. 파일 처리의 기본
- 안드로이드에서 파일을 읽고 쓰는 방법에는 Java에서 제공하는 파일 관련 클래스를 사용하는 방법과 안드로이드에서 제공하는 파일 관련 클래스를 사용하는 방법이 있습니다.
- 아무 곳에서나 파일을 읽고 쓸 수는 없고 제한된 폴더나 SD 카드 등에서만 가능합니다.
- PC용 운영체제에서 파일을 처리하는 것과 다르게 느껴질 수 있으나 모바일 기기의 특성을 고려한 적절한 방식이므로 보안에 도움이 됩니다.
1-1 내장 메모리 파일 처리
- 앱을 종료했다가 다음에 다시 실행할 때 사용했던 곳부터 이어서 작업하고 싶은 경우가 있습니다.
- 이럴 때 내장 메모리에 파일을 저장하고 읽어오는 방식을 활용합니다.
- 내장 메모리의 저장 위치는 /data/data/패키지명/files 폴더입니다.
- 일반적으로 응용 프로그램마다 다른 패키지명을 사용하므로 응용 프로그램별로 고유의 저장 공간이 있다고 생각하면 됩니다.
- 파일을 읽기 위해 먼저 안드로이드 Context 클래스의 openFileInput( ) 메서드를 사용하는데, 이 메서드는 FileInputStream을 반환합니다.
- 파일을 쓰기 위해 openFileOutput( ) 메서드를 사용하면 FileOutputStream을 반환합니다.
- Java에서 제공하는 파일을 읽거나 쓰는 java.io.FileInputStream 클래스와 java.io.FileOutputStream의 read( ), write( ) 메서드를 사용하여 파일을 처리합니다.
- 아래는 내장 메모리에서 파일을 읽거나 쓰는 일반적인 절차를 간단하게 정리했습니다.
예제 8-1 파일 처리의 기본 activity_main.xml
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="20dp"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnWrite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내장 메모리에 파일 쓰기" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnRead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내장 메모리에서 파일 읽기" />
</androidx.appcompat.widget.LinearLayoutCompat>
예제 8-2 파일 처리의 기본 Java 코드
public class MainActivity extends AppCompatActivity{
private Button btnWrite, btnRead;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
initLr();
}
public void init(){
btnWrite = findViewById(R.id.btnWrite);
btnRead = findViewById(R.id.btnRead);
}
public void initLr(){
btnWrite.setOnClickListener(v -> {
try {
FileOutputStream outFs = openFileOutput("file.txt",
Context.MODE_PRIVATE);
String str = "안드로이드";
outFs.write(str.getBytes());
outFs.close();
Toast.makeText(getApplicationContext(), "file.txt가 생성됨",
Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
});
btnRead.setOnClickListener(v -> {
try {
FileInputStream inFs = openFileInput("file.txt");
byte[] txt = new byte[30];
inFs.read(txt);
String str = new String(txt);
Toast.makeText(getApplicationContext(), str,
Toast.LENGTH_SHORT).show();
inFs.close();
} catch (IOException e) {
Toast.makeText(getApplicationContext(), "파일 없음",
Toast.LENGTH_SHORT).show();
}
});
}
}
실습 8-1 간단 일기장 앱 만들기
예제 8-3 activity_main.xml 코드
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity">
<DatePicker
android:id="@+id/datePicker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner"/>
<EditText
android:id="@+id/etDiary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_light"
android:lines="8" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnWrite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="작성하기" />
</androidx.appcompat.widget.LinearLayoutCompat>
예제 8-4, 5, 6, 7 MainActivity.java 코드
public class MainActivity extends AppCompatActivity {
private DatePicker datePicker;
private EditText etDiary;
private Button btnWrite;
private String fileName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
initLr();
}
public void init(){
datePicker = findViewById(R.id.datePicker);
etDiary = findViewById(R.id.etDiary);
btnWrite = findViewById(R.id.btnWrite);
Calendar cal = Calendar.getInstance();
int cYear = cal.get(Calendar.YEAR);
int cMonth = cal.get(Calendar.MONTH);
int cDay = cal.get(Calendar.DAY_OF_MONTH);
datePicker.init(cYear, cMonth, cDay, new DatePicker.OnDateChangedListener() {
@Override
public void onDateChanged(DatePicker datePicker, int year,
int monthOfYear, int dayOfMonth) {
fileName = Integer.toString(year) + "-" + Integer.toString(monthOfYear + 1)
+ "-" + Integer.toString(dayOfMonth) + ".txt";
String str = readDiary(fileName);
etDiary.setText(str);
btnWrite.setEnabled(true);
}
});
}
public void initLr(){
btnWrite.setOnClickListener(v -> {
try {
FileOutputStream outFs = openFileOutput(fileName, Context.MODE_PRIVATE);
String str = etDiary.getText().toString();
outFs.close();
Toast.makeText(getApplicationContext(), fileName + "이 저장됨",
Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
});
}
String readDiary(String fName) {
String diaryStr = null;
FileInputStream inFs;
try {
inFs = openFileInput(fName);
byte[] txt = new byte[500];
inFs.read(txt);
inFs.close();
diaryStr = (new String(txt)).trim();
btnWrite.setText("수정하기");
} catch (IOException e) {
etDiary.setHint("일기 없음");
btnWrite.setText("새로 저장");
}
return diaryStr;
}
}
- 실행은 되지만 에러가 발생해서 앱이 종료되거나, 일기 내용이 입력되지 않으므로 해결방안 찾기
1-2 raw 폴더 파일 처리
- 프로제긑의 /res/raw 폴더에 필요한 파일을 저장하여 사용하는 방법도 있습니다.
- 기본적으로 /res 아래에 raw 폴더를 생성하고 프로젝트에서 사용할 파일을 넣어두면 됩니다.
- Java 코드에서 openRawResource( ) 메서드를 사용하여 접근할 수 있으며, FileInputStream 클래스 대신 InputStream 클래스를 사용합니다.
- /res/raw는 프로젝트에 포함된 폴더이므로 읽기 전용으로만 사용 가능하고 쓰기는 할 수 없습니다.
- 쓰기에는 내장 메모리나 SD 카드를 사용해야 합니다.
예제 8-8 /res/raw 폴더의 파일 읽기 activity_main.xml 코드
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnRead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="/res/raw에서 파일 읽기" />
<EditText
android:id="@+id/etRaw"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="10" />
</androidx.appcompat.widget.LinearLayoutCompat>
예제 8-9 /res/raw 폴더의 파일 읽기 Java 코드
public class MainActivity extends AppCompatActivity {
private Button btnRead;
private EditText etRaw;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("간단 일기장");
init();
initLr();
}
public void init(){
btnRead = findViewById(R.id.btnRead);
etRaw = findViewById(R.id.etRaw);
}
public void initLr(){
btnRead.setOnClickListener(v -> {
try {
InputStream inputStream = getResources().openRawResource(R.raw.chapter8);
byte[] txt = new byte[inputStream.available()];
inputStream.read(txt);
etRaw.setText(new String(txt));
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
2. 파일 처리의 응용
- 안드로이드 내부의 파일을 처리하는 것은 앞에서 했던 것과 같이 /data/data/패키지명/files 폴더를 사용합니다.
- 읽기 전용 파일의 경우 /res/raw 폴더를 사용할 수 있습니다.
- 음악, 영상, 그림 파일 등은 여러 응용 프로그램에서 사용되는 경우가 많습니다.
- MP3 파일을 안드로이드에 내장된 앱에서 들을 수도 있고 별도의 음악 플레이어로 들을 수도 있습니다.
- 이런 경우 SD 카드에 저장하여 활용해야 합니다.
- 안드로이드에서는 SD 카드에 저장된 데이터에 특별한 인증 절차 없이 접근할 수 있습니다.
- 제한된 공간의 내장 메모리보다 훨씬 큰 공간을 사용할 수 있으며 확장성도 뛰어납니다.
- 실제 안드로이드 폰의 경우 주로 마이크로 SD 카드를 장착하여 사용합니다.
- AVD에서 가상 SD 카드를 장착할 수 있는데, AVD를 생성할 때 512MB의 가상 SD 카드가 장착되었습니다.
- 메뉴에서 [Tools] - [AVD Manger]를 선택하여 해당 AVD 이름의 오른쪽에 있는 Edit 아이콘을 클릭한 후 <Show Advanced Settings>를 클릭하면 사용하는 AVD에 장착된 SD 카드를 확인할 수 있습니다.
- 만약 가상 SD 카드가 만들어져 있지 않으면 원하는 크기를 새로 입력하면 됩니다.
2-1 SD 카드에서 파일 읽기
- 폴더 경로가 다른 것만 제외하면 SD 카드의 기본적인 경로는 내장 메모리에서 파일을 읽을 떄와 별 차이가 없습니다.
- 먼저 [View] - [Tool Window] - [Device File Explorer]에서 /sdcard 폴더 또는 /storage/emulated/0 폴더에 적당한 텍스트 파일을 하나 넣습니다.(Upload)
- AVD에서 SD 카드의 경로는 절대적인 것이 아니며 SDK 버전에 따라 달라질 수 있습니다.
- AndroidManifest.xml 파일에 SD 카드를 사용할 수 있도록 퍼미션을 지정해야 합니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cos.프로젝트명">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:requestLegacyExternalStorage="true"
...>
....
</application>
</manifest>
- 위와 같이 AndroidManifest.xml에 퍼미션 및 application 관련 속성을 추가합니다.
- 마시멜로(Android 6.0. API 23)부터는 보안 모델이 강화되어 각 앱마다 파일, 사진, 미디어 등에 액세스할 수 있도록 코딩해야 합니다.
- ActivityCompat.requestPermissions( ) 메서드를 사용하면 됩니다.
- 위 메서드는 사용자가 거부 / 허용을 선택할 수 있는 대화상자가 나오게 하는데, <허용>을 클릭해야 파일 등에 접근이 가능합니다.
예제 8-10 SD 카드에서 파일 읽기 activity_main.xml
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnRead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SD 카드에서 파일 읽기" />
<EditText
android:id="@+id/etSD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="10" />
</androidx.appcompat.widget.LinearLayoutCompat>
예제 8-11 SD 카드에서 파일 읽기 Java 코드
public class MainActivity extends AppCompatActivity {
private Button btnRead;
private EditText etSD;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle(null);
init();
initLr();
}
public void init(){
btnRead = findViewById(R.id.btnRead);
etSD = findViewById(R.id.etSD);
ActivityCompat.requestPermissions(this, new String[]
{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, MODE_PRIVATE);
}
public void initLr(){
btnRead.setOnClickListener(v -> {
try {
FileInputStream inFs = new FileInputStream("/sdcard/test.txt");
byte[] txt = new byte[inFs.available()];
inFs.read(txt);
etSD.setText(new String(txt));
inFs.close();
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
실습 8-2 간단 이미지 뷰어 만들기
예제 8-16, 17 myPictureView 클래스의 Java 코드
public class myPictureView extends View {
// myPictureView 에 보여줄 이미지 파일의 경로 및 파일 이름을 저장할 변수
String imagePath = null;
public myPictureView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// imagePath 에 값이 있으면 화면에 그림 파일을 출력함
if(imagePath != null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
canvas.drawBitmap(bitmap, 0, 0, null);
bitmap.recycle();
}
}
}
예제 8-18 activity_main.xml 코드
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnPrev"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:text="이전 그림" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnNext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:text="다음 그림" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.cos.project8_2.myPictureView
android:id="@+id/myPictureView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>
예제 8-19, 20, 21 MainActivity.java 코드
public class MainActivity extends AppCompatActivity {
private Button btnPrev, btnNext;
myPictureView myPictureView;
int num = 1;
File[] imageFiles;
String imageFname;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("간단 이미지 뷰어");
ActivityCompat.requestPermissions(this, new String[]
{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MODE_PRIVATE);
btnPrev = findViewById(R.id.btnPrev);
btnNext = findViewById(R.id.btnNext);
myPictureView = findViewById(R.id.myPictureView);
imageFiles = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Pictures").listFiles();
imageFname = imageFiles[1].toString();
myPictureView.imagePath = imageFname;
btnPrev.setOnClickListener(v -> {
if(num <= 1){
Toast.makeText(getApplicationContext(), "첫번째 그림입니다.",
Toast.LENGTH_SHORT).show();
} else {
num--;
imageFname = imageFiles[num].toString();
myPictureView.imagePath = imageFname;
myPictureView.invalidate();
}
});
btnNext.setOnClickListener(v -> {
if(num >= imageFiles.length - 1){
Toast.makeText(getApplicationContext(), "마지막 그림입니다.",
Toast.LENGTH_SHORT).show();
} else {
num++;
imageFname = imageFiles[num].toString();
myPictureView.imagePath = imageFname;
myPictureView.invalidate();
}
});
}
}
Mainfest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cos.project8_2">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Project8_2">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>