Data path and method (저장소 경로 및 경로 획득 함수)
이 글은 이 글을 참고하여 작성하였다.
안드로이드 시스템에서 데이터 저장 장소는 크게 2가지로 나뉘어진다.
첫번째는 내부 저장소로 이 공간은 해당 앱에서만 접근 가능하다. 유저도 이 공간에는 직접 접근할 수 없다.
두번째는 외부 저장소로 외부저장소는 공용영역과 고유영역으로 나뉘어진다.
외부저장소의 공용영역은 다양한 어플이 접근할 수 있는 영역이며 어플이 삭제되어도 이 경로에 저장된 데이터는 유지된다.
반면 외부저장소의 고유영역은 다른 어플도 접근 가능하며, 어플이 삭제시 이 공간의 데이터도 삭제된다.
보안을 위해서는 외부 어플이 접근할 수 없도록 내부 저장소에 저장하는 것이 안전하며,
만약 내부 저장소에 저장된 데이터를 외부 어플로 전달하고 싶은경우 (email 첨부, 다른어플로 열기 등)에는 fileprovider를 사용해야 한다.
fileprovider를 이용해서 데이터를 전달하는 방법에 대해서는 다음 글을 참고하기 바란다.
내부저장소
각 애플리케이션에서만 데이터를 읽고 쓸 수 있다.
1. 캐시(Cache)
임시 파일들이 저장된다.
File Context.getCacheDir()
내부 저장소의 캐시 디렉터리 경로를 반환한다.
경로: /data/data/패키지 이름/cache
2. 데이터베이스(Database)
데이터베이스 파일들이 저장된다.
File Context.getDatabasePath(String name)
데이터베이스 파일의 경로를 반환. 인자로 데이터베이스 파일의 이름을 넘겨준다.
경로: /data/data/패키지 이름/databases
3. 일반 파일
일반 파일이 저장되는 영역이다.
이 경로는 Context.openFileOutput(String, int)를 사용하여 생성되는 파일이 저장되는 경로와 동일하다.
File Context.getFilesDir()
일반 파일들의 저장 경로를 반환한다.
경로: /data/data/패키지 이름/files
각 일반 파일들의 경로를 가져오기
File Context.getFileStreamPath(String name)
일반 파일이 저장된 공간에서 특정 이름을 가지는 파일의 경로를 반환한다.
인자로 확장자를 포함한 파일 이름을 넘겨준다.
경로: /data/data/패키지 이름/files/파일이름
외부저장소 (공용영역)
애플리케이션을 삭제해도 데이터는 남아있다.
1. 최상위 경로
외부 저장소(SD카드)의 최상위 경로를 반환한다.
static File Environment.getExternalStorageDirectory()
경로: /mnt/sdcard 또는 /storage/emulated/0 등 기종마다 다르다.
2. 특정 데이터를 저장하는 영역
여러 애플리케이션에서 공용으로 사용할 수 있는 데이터들을 저장하며 데이터의 유형에 따라 별도의 디렉터리를 사용한다.
이 영역에 데이터를 저장하기 전에, 해당 디렉터리가 존재하는지 확인해야 한다. 존재하지 않으면 FileNotFoundException이 발생하기 때문에 File.mkdirs()를 사용하여 없을 경우 새 디렉터리를 생성해 준다.
다음의 함수를 이용해서 external 디렉터리가 존재하는지, 그리고 write가 가능한지 확인할 수 있다.
public static boolean checkAvailable() {
// Retrieving the external storage state
String state = Environment.getExternalStorageState();
// Check if available
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
public static boolean checkWritable() {
// Retrieving the external storage state
String state = Environment.getExternalStorageState();
// Check if writable
if (Environment.MEDIA_MOUNTED.equals(state){
if(Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return false;
}else{
return true;
}
}
return false;
}
외부 저장소의 해당 경로를 얻기위해서는 다음 함수를 사용한다
static File Environment.getExternalStoragePublicDirectory(String type)
인자로 디렉터리의 유형을 넘겨준다.
[각 인자 및 경로]
Environment.DIRECTORY_ALARMS : 알람으로 사용할 오디오 파일 저장 (/mnt/sdcard/Alarms)
Environment.DIRECTORY_DCIM : 카메라로 촬영한 사진 저장 (/mnt/sdcard/DCIM)
Environment.DIRECTORY_DOWNLOADS : 다운로드한 파일 저장 (/mnt/sdcard/Download)
Environment.DIRECTORY_MUSIC : 음악 파일 저장 (/mnt/sdcard/Music)
Environment.DIRECTORY_MOVIES : 영상 파일 저장 (/mnt/sdcard/Movies)
Environment.DIRECTORY_NOTIFICATIONS : 알림음으로 사용할 오디오 파일 저장 (/mnt/sdcard/Notifications)
Environment.DIRECTORY_PICTURES : 그림 파일 저장 (/mnt/sdcard/Pictures)
Environment.DIRECTORY_PODCASTS : 팟캐스트 파일 저장 (/mnt/sdcard/Podcasts)
외부저장소 (고유영역)
외부 저장소 - 애플리케이션 고유 영역
이 영역에 저장된 데이터는 애플리케이션이 삭제될 때 같이 삭제되며 다른 애플리케이션에서 해당 데이터에 접근하는 것이 가능하다.
1. 특정 데이터를 저장하는 영역
애플리케이션 고유 영역에도 공용 영역과 마찬가지로 각 데이터 유형별로 데이터를 저장하는 표준 디렉터리를 제공한다.
File Context.getExternalFilesDir(String type)
인자로 디렉터리의 유형을 넘겨준다.
[각 인자 및 경로]
Environment.DIRECTORY_ALARMS (/mnt/sdcard/Android/data/패키지 이름/files/Alarms)
Environment.DIRECTORY_DCIM (/mnt/sdcard/Android/data/패키지 이름/files/DCIM)
Environment.DIRECTORY_DOWNLOADS (/mnt/sdcard/Android/data/패키지 이름/files/Download)
Environment.DIRECTORY_MUSIC (/mnt/sdcard/Android/data/패키지 이름/files/Music)
Environment.DIRECTORY_MOVIES (/mnt/sdcard/Android/data/패키지 이름/files/Movies)
Environment.DIRECTORY_NOTIFICATIONS (/mnt/sdcard/Android/data/패키지 이름/files/Notifications)
Environment.DIRECTORY_PICTURES (/mnt/sdcard/Android/data/패키지 이름/files/Pictures)
Environment.DIRECTORY_PODCASTS (/mnt/sdcard/Android/data/패키지 이름/files/Podcasts)
null (/mnt/sdcard/Android/data/패키지 이름/files)
2. 캐시 데이터를 저장하는 영역
애플리케이션에서 사용하는 임시 데이터를 외부 저장소에 저장한다.
File Context.getExternalCacheDir()
경로: /mnt/sdcard/Android/data/패키지 이름/cache
- 외부 저장소는 사용자 데이터(사진, 음악 등)이 저장되는 영역이다. 일반적으로 단말기의 외장 SD카드를 지칭하지만, 단말기에 따라서는 이 영역이 외장 SD카드가 아닌 단말기 내부에 탑재되어 있는 경우도 있다.(넥서스S) 또는, 단말기 내에 탑재된 외장 메모리 영역 외에 별도의 SD카드도 지원하는 단말기도 있다.(갤럭시S)
export file in internal storage (내부 저장소의 데이터 출력, fileprovider)
안드로이드에서는 보안을 위해서 데이터를 내부에 저장하는 것이 가장 안전한 방법이다.
안드로이드의 데이터 저장위치에 대한 내용을 검색해 보면 크게
내부저장소, 외부저장소 (공유영역, 어플고유영역)으로 나뉘어 진다.
내부저장소는 어플 자체에서만 접근 가능하며, 사용자도 접근하지 못한다.
외부저장소의 공유영역은 모든 어플에서 접근 가능하며, 어플을 삭제해도 데이터가 남는 영역이다.
외부저장소의 고유영역은 모든 어플에서 접근 가능하지만, 어플을 삭제할때 데이터도 같이 삭제되는 영역이다.
이러한 각 데이터 저장 영역에 대해서는 많은 포스팅이 있으니 참고하자.
http://jinyongjeong.github.io/2018/09/30/android_path/
http://ismydream.tistory.com/124
http://bitsoul.tistory.com/117
https://developer88.tistory.com/28
만약 보안을 위해서 내부영역에 데이터를 저장했는데 이메일을 통해서 데이터를 출력하고 싶다면 어떻게 해야할까.
가장 쉽게 생각할 수 있는것이 외부영역으로 데이터를 복사한 후에 데이터를 출력하고 삭제하는 방법이다.
하지만 이럴때 사용할 수 있는 것이 fileprovider
이다.
fileprovider
는 내부 영역에 특정 파일을 외부 어플에서 접근 가능하도록 제공해 주는 역할을 하며
특정 파일을 지정해서 공유할 수 있기 때문에 안전하다.
fileprovider
를 사용하기 위해서는 다음과 같은 절차가 필요하다.
1. Manifest 설정
AndroidManifest.xml
의 application
안쪽에 선언
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
2. Provider의 경로 설정
res/xml/file_path.xml
을 생성하고 다음을 입력
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<files-path path="/" name="default" />
</paths>
</paths>
경로가 xml
폴더가 아닐경우 오류가 난다
3. Email을 통해 데이터 출력
File file = new File(activity.getFilesDir(), "/exported_data.zip");
Uri uri = FileProvider.getUriForFile(context, activity.getPackageName()+".fileprovider", file);
Intent i = new Intent(Intent.ACTION_SEND);
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.setType("message/rfc822");
i.putExtra(Intent.EXTRA_SUBJECT, "Gaitspeedometer data");
i.putExtra(Intent.EXTRA_STREAM, uri);
try {
startActivity(Intent.createChooser(i, "Send file..."));
} catch (Exception e) {
Toast.makeText(activity, "No activity found for export", Toast.LENGTH_SHORT).show();
}
위와 같이 코드를 실행하면 내부 영역에 있는 exported_data.zip
파일이 이메일에 첨부되고 출력할 수 있게 된다.
Ubuntu 16.04 Wifi problem in Macbook
Macbook에 듀얼부팅으로 ubuntu16.04를 설치하였을 때 Wifi가 정상적으로 잡히지 않는 문제가 발생한다.
구글링을 해보면 다음과 같은 답변이 많이 나오는데 나의경우에는 해결되지 않았다.
sudo apt-get install firmware-b43-installer
sudo modprobe -r b43
sudo modprobe b43
위와 같은 방법 말고 bcmwl 커널소스를 재 설치 하는 방법으로 해결되었다.
혹시 이글을 보고 있다면 위에 코드 실행하기 전에 아래 코드를 먼저 실행해 보기 바란다.
sudo apt-get update
sudo apt-get install --reinstall bcmwl-kernel-source
기타 Mac 에 우분투를 설치시 다양한 문제들에 대한 정보는
다음 링크에서 찾아볼수 있다.
Macbook command key to alt
맥북에 ubuntu를 설치해서 사용하면 command키가 은근 거슬린다.
원래 alt자리에 command키가 있기 때문에 평소처럼 command키를 alt키로 사용하고 싶다.
그럴때는 gnome-tweak-tool
을 이용하자
sudo apt-get update
sudo apt-get install gnome-tweak-tool
설치를 한 후 실행한다. 터미널을 열고 실행
실행 후 typing
탭에서 alt/win key behavior
을 조정해 주면되는데
alt is mapped to win keys
로 설정해 주면 command키가 alt키로 변경된다.
How to pairing bluetooth device
이 글에서는 안드로이드 어플에서 직접 주변에 있는 기기들을 검색하고 pairing을 수행하는 방법에 대해서 설명한다.
이전 글에서는 페어링이 되어있는 기기 목록에서 사용자가 선택하여 기기에 연결하는 방법에 대해서 설명하였다.
이 글에서 사용된 블루투스 연결 기기는 일반적으로 많이 사용되는 HC-06 블루투스 모듈이다.
1. 블루투스 관련 권한 설정
안드로이드 디바이스에서 블루투스 관련 기능을 사용하기 위해서는 블루투스 관련 권한 설정을 해야한다.
또한 블루투스 권한설정 뿐만 아니라 주변 기기의 검색을 위해서는 위치 접근에 대한 권한이 필요하다. 따라서 다음과 같이 4개에 대해서 접근 권한을 설정한다.
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
안드로이드 디바이스에서 블루투스 권한을 얻기 위해서는 AndroidManifest.xml
파일에 위의 코드 두줄을 추가해 줘야 한다.
2. 블루투스 기기 검색에 대한 IntentFilter 설정
안드로이드의 IntentFilter는 안드로이드 시스템이나 다른 어플에서 broadcast하는 메세지를 받을 수 있도록 설정하는 것이다. 즉 어떤 메세지를 날렸을 때 받을 것인지를 설정하는 것이다. 아래 코드는 블루투스의 상태변화, 연결됨, 연결 끊어짐 등에 대한 설정 뿐만 아니라 기기 검색 시작, 종료, 검색됨 등에 대한 메세지도 받을 수 있도록 설정하는 코드이다.
IntentFilter stateFilter = new IntentFilter();
stateFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); //BluetoothAdapter.ACTION_STATE_CHANGED : 블루투스 상태변화 액션
stateFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
stateFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //연결 확인
stateFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); //연결 끊김 확인
stateFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
stateFilter.addAction(BluetoothDevice.ACTION_FOUND); //기기 검색됨
stateFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //기기 검색 시작
stateFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //기기 검색 종료
stateFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
registerReceiver(mBluetoothStateReceiver, stateFilter);
registerReceiver
는 intentfilter를 특정 broadcast receiver로 연결시켜주는 역할을 한다. 이 코드에서는 mBluetoothStateReceiver
라는 broadcast receiver에 연결시켜 주었다. 만약 앱이 비활성화 상태일 때 broadcast를 수신하지 않도록 하기 위해서는 onPause
와 같은 함수에서 unregister를 해주어야 한다.
unregisterReceiver(bluetoothRequest.mBluetoothStateReceiver);
3. Broadcast Receiver 정의
다음으로는 Intentfilter에 의해서 메세지를 수신할 broadcast receiver를 정의해야 한다.
BroadcastReceiver mBluetoothStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction(); //입력된 action
Toast.makeText(context, "받은 액션 : "+action , Toast.LENGTH_SHORT).show();
Log.d("Bluetooth action", action);
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String name = null;
if (device != null) {
name = device.getName(); //broadcast를 보낸 기기의 이름을 가져온다.
}
//입력된 action에 따라서 함수를 처리한다
switch (action){
case BluetoothAdapter.ACTION_STATE_CHANGED: //블루투스의 연결 상태 변경
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch(state) {
case BluetoothAdapter.STATE_OFF:
break;
case BluetoothAdapter.STATE_TURNING_OFF:
break;
case BluetoothAdapter.STATE_ON:
break;
case BluetoothAdapter.STATE_TURNING_ON:
break;
}
break;
case BluetoothDevice.ACTION_ACL_CONNECTED: //블루투스 기기 연결
break;
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED: //블루투스 기기 끊어짐
break;
case BluetoothAdapter.ACTION_DISCOVERY_STARTED: //블루투스 기기 검색 시작
break;
case BluetoothDevice.ACTION_FOUND: //블루투스 기기 검색 됨, 블루투스 기기가 근처에서 검색될 때마다 수행됨
String device_name = device.getName();
String device_Address = device.getAddress();
//본 함수는 블루투스 기기 이름의 앞글자가 "GSM"으로 시작하는 기기만을 검색하는 코드이다
if(device_name != null && device_name.length() > 4){
Log.d("Bluetooth Name: ", device_name);
Log.d("Bluetooth Mac Address: ", device_Address);
if(device_name.substring(0,3).equals("GSM")){
bluetooth_device.add(device);
}
}
break;
case BluetoothDevice.ACTION_DISCOVERY_FINISHED: //블루투스 기기 검색 종료
Log.d("Bluetooth", "Call Discovery finished");
StartBluetoothDeviceConnection(); //원하는 기기에 연결
break;
case BluetoothDevice.ACTION_PAIRING_REQUEST:
break;
}
}
};
위와 같이 주변의 블루투스 기기를 검색해서 처리하기 위한 broadcast receiver를 등록하였다.
주변에 모든 블루투스 기기를 모두 검색하기 위해서는 약 11초 전후의 시간이 걸리며 action의 순서는 다음과 같다.
사용자 검색 요청 -> BluetoothAdapter.ACTION_DISCOVERY_STARTED
-> BluetoothDevice.ACTION_FOUND
(검색되는 기기 갯수대로) -> BluetoothDevice.ACTION_DISCOVERY_FINISHED
따라서 검색되는 기기에 대한 저장을 한 후에 BluetoothDevice.ACTION_DISCOVERY_FINISHED
가 수행되었을 때 원하는 기기에 접근해서 페어링을 수행하면 된다.
4. 블루투스 기기 검색 요청
블루투스 기기에 대한 요청을 하기 위해서는 bluetooth adapter 객체를 가지고 있어야 한다.
이전 블루투스 연결 글에서 처럼 다음과 같은 맴버 변수가 있으며 다음과 같이 검색 수행을 시작시킬 수 있다.
private static final int REQUEST_ENABLE_BT = 3;
public BluetoothAdapter mBluetoothAdapter = null;
Set<BluetoothDevice> mDevices;
int mPairedDeviceCount;
BluetoothDevice mRemoteDevice;
BluetoothSocket mSocket;
InputStream mInputStream;
OutputStream mOutputStream;
Thread mWorkerThread;
int readBufferPositon; //버퍼 내 수신 문자 저장 위치
byte[] readBuffer; //수신 버퍼
byte mDelimiter = 10;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); //블루투스 adapter 획득
mBluetoothAdapter.startDiscovery(); //블루투스 기기 검색 시작
mBluetoothAdapter.startDiscovery()
가 실행되면 블루투스 기기 검색이 시작되며 broadcast receiver로 메세지가 들어오게 된다.
이제 검색이 시작되었으니 검색이 종료되었을 때, 즉 BluetoothDevice.ACTION_DISCOVERY_FINISHED
이 수행될 때 검색된 목록 중에서
원하는 기기로 연결을 하면 된다.
본 예제에서는 검색 종료시 StartBluetoothDeviceConnection()
함수가 수행되었다.
5. 검색된 블루투스 기기에 페어링 및 연결
앞에서 검색된 기기의 이름은 bluetooth_device
라는 맴버변수에 저장되어 있다. 이제 이 리스트 중에서 연결한 기기를 사용자가 선택할 수 있도록 해준다.
그리고 사용자가 선택할 경우 그 기기에 연결을 수행한다.
public void StartBluetoothDeviceConnection(){
//Bluetooth connection start
if(bluetooth_device.size() == 0){
Toast.makeText(activity,"There is no device", Toast.LENGTH_SHORT).show();
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_device_title);
// 페어링 된 블루투스 장치의 이름 목록 작성
List<String> listItems = new ArrayList<String>();
for (BluetoothDevice bt_device : bluetooth_device) {
listItems.add(bt_device.getName());
}
listItems.add("Cancel"); // 취소 항목 추가
final CharSequence[] items = listItems.toArray(new CharSequence[listItems.size()]);
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Dialog dialog_ = (Dialog) dialog;
if (which == bluetooth_device.size()) {
// 연결할 장치를 선택하지 않고 '취소'를 누른 경우
} else {
// 기기 이름을 선택한 경우 선택한 기기 이름과 같은 블루투스 객체를 찾아서 연결을 시도한다
for (BluetoothDevice bt_device : bluetooth_device) {
if (bt_device.getName().equals(items[which].toString())) {
Log.d("Bluetooth Connect", bt_device.getName());
ConnectBluetoothDevice(bt_device); //해당하는 블루투스 객체를 이용하여 연결 시도
Log.d("Bluetooth Connect", "ConnectBluetoothDevice");
break;
}
}
}
}
});
builder.setCancelable(false); // 뒤로 가기 버튼 사용 금지
AlertDialog alert = builder.create();
alert.show();
Log.d("Bluetooth Connect", "alert start");
}
6. 해당 기기에 연결
이제 사용자가 선택한 블루투스 객체를 이용해서 해당 블루투스 기기에 연결 혹은 pairing을 수행해야 한다.
다음 함수는 이러한 역할을 수행한다.
public void ConnectBluetoothDevice(final BluetoothDevice device){
mDevices = mBluetoothAdapter.getBondedDevices();
mPairedDeviceCount = mDevices.size();
//pairing되어 있는 기기의 목록을 가져와서 연결하고자 하는 기기가 이전 기기 목록에 있는지 확인
boolean already_bonded_flag = false;
if(mPairedDeviceCount > 0){
for (BluetoothDevice bonded_device : mDevices) {
if(activity.bluetooth_device_name.equals(bonded_device.getName())){
already_bonded_flag = true;
}
}
}
//pairing process
//만약 pairing기록이 있으면 바로 연결을 수행하며, 없으면 createBond()함수를 통해서 pairing을 수행한다.
if(!already_bonded_flag){
try {
//pairing수행
device.createBond();
} catch (Exception e) {
e.printStackTrace();
}
}else{
connectToSelectedDevice(device);
}
}
위 코드처럼 이전 연결 목록을 확인하여 처음 연결되는 기기는 createBond()
함수를 통해 페어링을 수행하며, 처음 연결이 아닌 경우에는 바로 연결을 수행한다.
연결을 수행하기 위한 함수인 connectToSelectedDevice
는 이전 글에서 확인할 수 있다.
물론 이러한 블루투스에 연결되는 과정들은 시간이 소요되는 과정들이기 때문에 연결 중에는 로딩화면을 띄워주면 좋다.
위의 코드에서는 로딩 화면을 띄워주는 부분은 포함하고 있지 않다.
Pagination