2019년 3월 12일 화요일

안드로이드 프로젝트 gitignore 리스트

- 프로젝트 복사와 SVN 업로드 시에 불필요한 부분을 덜어내야 관리하기 편리합니다.


1. gitignore 목록
- 프로젝트 루트 디렉토리의 .gitignore 파일은 아래와 같이 설정
*.iml
.gradle/
.idea
/*/build/
/local.properties
/captures
.externalNativeBuild
.DS_Store
Thumbs.db

> app 디렉토리의 .gitignore 파일이 아닌 루트 디렉토리의 .gitignore 파일 입니다.
> 루트 디렉토리의 .gitignore이 없을 경우 새로 생성 하거나 안드로이드 스튜디오에서 직접 gitignore 항목을 지정합니다.




2. 프로젝트 압축 시 용량 줄이기
- 프로젝트 루트 디렉토리





























.gradle 디렉토리 제외
.idea 디렉토리 제외
local.properties 파일 제외
.iml 확장자 제외




- 어플리케이션 디렉토리






























build 디렉토리 제외
.iml 확장자 제외

콜백 인터페이스 - ResultReceiver

- ResultReceiver를 더 자세히 이해하기 위해서는 안드로이드 IPC 자료를 찾아볼 것.
ResultReceiver 클래스는 Parcelable 인터페이스를 구현하므로 Parcelable 객체를 전달 받을 수 있는 모든 곳에서 콜백을 처리할 수 있다.


1. ResultReceiver 기본코드
- ResultReceiver 콜백
ResultReceiver resultReceiver = new ResultReceiver(textView.getHandler()) {
    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        super.onReceiveResult(resultCode, resultData);
        // 콜백
        if (resultCode == RESULT_OK && resultData != null) {
            String text = resultData.getString("message");
            textView.setText(text);
        }
    }
};



ResultReceiver 콜백 파라미터
BlankFragment fragment = new BlankFragment();
Bundle args = new Bundle();
args.putParcelable(ResultReceiver.class.getSimpleName(), resultReceiver);
fragment.setArguments(args);

또는

Intent intent = new Intent(getBaseContext(), SubActivity.class);
intent.putExtra(ResultReceiver.class.getSimpleName(), resultReceiver);
startActivity(intent);


ResultReceiver가 Parcelable를 구현하므로 Parcelable를 수신할 수 있는 모든 곳에서 콜백을 동작시킬 수 있다.
> 주로 Activity와 Service 간 통신에 사용된다.



- Bundle 전송
ResultReceiver resultReceiver = getArguments().getParcelable(ResultReceiver.class.getSimpleName());
Bundle bundle = new Bundle();
bundle.putString("message", "Hello, World!");
resultReceiver.send(Activity.RESULT_OK, bundle);


> sned를 통해 Bundle를 전송하면 onReceiveResult 메소드에서 수신한다.




2. 샘플코드
소스코드
APK


안드로이드 관리자 권한 예제 - DevicePolicyManager

- 관리자 권한 취득
- 카메라 사용 제한하기
- 앱 삭제 제한하기(어플리케이션 관리 화면에서는 삭제 가능)


1. 관리자 권한
- xml 정의 - device_admin_receiver.xml
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
        <disable-camera />
    </uses-policies>
</device-admin>

> res 디렉토리의 xml 디렉토리에 xml 문서 생성
> 카메라 제어를 위해 <disable-camera /> 적용
> 레퍼런스 :
https://developer.android.com/guide/topics/admin/device-admin
> 안드로이드 9 버전 이후 부터 변경 사항이 있습니다.
https://developers.google.com/android/work/device-admin-deprecation


- DeviceAdminReceiver  상속
public class AppDeviceAdminReceiver extends DeviceAdminReceiver {
    private static final String TAG = AppDeviceAdminReceiver.class.getSimpleName();

    @Override
    public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
        Log.d(TAG, "Admin onEnabled");
    }

    @Override
    public void onDisabled(Context context, Intent intent) {
        super.onDisabled(context, intent);
        Log.d(TAG, "Admin onDisabled");
    }
}


- 리시버 등록 - AndroidManifest.xml
<receiver
android:name=".AppDeviceAdminReceiver"
android:description="@string/device_admin_description"
android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_receiver" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>


- 관리자 권한 요청
ComponentName componentName = new ComponentName(this, AppDeviceAdminReceiver.class);
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName);
startActivityForResult(intent, DEVICE_ADMIN_ADD_RESULT_ENABLE);


> 권한 확인
final boolean adminActive = devicePolicyManager.isAdminActive(componentName);

> 카메라 제어 확인
boolean cameraDisabled = devicePolicyManager.getCameraDisabled(componentName);




2. 샘플코드
소스코드
APK

 



2019년 3월 8일 금요일

Room 아키텍처 예제

- 아키텍처 구조


- 구글 코드랩 예제 :
https://codelabs.developers.google.com/codelabs/android-room-with-a-view/index.html?index=..%2F..index#0




1. 기본 구성
- dependencies 구성
def room_version = "2.1.0-alpha04"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

def lifecycle_version = "2.0.0"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

> 라이프사이클
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko#declaring_dependencies
> Room
https://developer.android.com/jetpack/androidx/releases/room?hl=ko



- Entity
@Entity(tableName = "user_table")
public class User {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int id;

    @NonNull
    @ColumnInfo(name = "userName")
    private String userName;

    @NonNull
    @ColumnInfo(name = "age")
    private int age;

}

> autoGenerate를 적용하면 insert 과정에서 id 값이 자동으로 증가한다.



- Dao
@Dao
public interface UserDao {
    @Insert
    long insert(User user);

    @Update
    int update(User user);

    @Query("DELETE FROM user_table")
    int deleteAll();

    @Query("DELETE FROM user_table WHERE id = :id")
    int deleteUser(int id);

    @Query("SELECT * from user_table ORDER BY userName ASC")
    LiveData<List<User>> getAllUsers();

}

> LiveData를 사용하지 않을 경우
    @Query("SELECT * from user_table ORDER BY userName ASC")
    List<User> getAllUsers();



- Database
@Database(entities = {User.class}, version = 1)
public abstract class UserRoomDatabase extends RoomDatabase {
    public abstract UserDao userDao();

    private static volatile UserRoomDatabase INSTANCE;

    static UserRoomDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (UserRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(), UserRoomDatabase.class, "user_database").build();
                }
            }
        }
        return INSTANCE;
    }
}


- 설명
androidx.room:room-compiler가 generatedJava 디렉토리의 소스코드를 자동 생성한다.

SQLiteOpenHelper를 사용했을 때 보다 더 생산성이 좋다.
링크 : 






2. 안드로이드 앱 아키텍처 적용



Room 아키텍처는 안드로이드 앱 아키텍처의 DB 연동 구성요소이다.


- Repository
class UserRepository {
    private static final String TAG = UserRepository.class.getSimpleName();

    private final UserDao userDao;
    private final LiveData<List<User>> allUsers;

    UserRepository(Application application) {
        UserRoomDatabase db = UserRoomDatabase.getDatabase(application);
        userDao = db.userDao();
        allUsers = userDao.getAllUsers();
    }

    public void insert(User user) {
        new AsyncTask<User, Void, Long>() {
            @Override
            protected Long doInBackground(User... users) {
                if (userDao == null)
                    return -1L;
                return userDao.insert(users[0]);
            }

            @Override
            protected void onPostExecute(Long aLong) {
                super.onPostExecute(aLong);
                Log.d(TAG, "insert : " + aLong);
            }
        }.execute(user);
    }

    public void update(User user) {
        new AsyncTask<User, Void, Integer>() {
            @Override
            protected Integer doInBackground(User... users) {
                if (userDao == null)
                    return -1;
                return userDao.update(users[0]);
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "update : " + integer);
            }
        }.execute(user);
    }

    public void deleteAll() {
        new AsyncTask<Void, Void, Integer>() {
            @Override
            protected Integer doInBackground(Void... voids) {
                if (userDao == null)
                    return -1;
                return userDao.deleteAll();
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "deleteAll : " + integer);
            }
        }.execute();
    }

    public void deleteUser(int id) {
        new AsyncTask<Integer, Void, Integer>() {
            @Override
            protected Integer doInBackground(Integer... integers) {
                if (userDao == null)
                    return -1;
                return userDao.deleteUser(integers[0]);
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "deleteUser : " + integer);
            }
        }.execute(id);
    }

    public LiveData<List<User>> getAllUsers() {
        return allUsers;
    }
}

> 메인쓰레드에서 DB 연동을 사용하지 않아야 한다.
> LiveData를 사용하지 않는 곳은 AsyncTask를 적용한다.



- AndroidViewModel 
public class UserViewModel extends AndroidViewModel {
    private static final String TAG = UserViewModel.class.getSimpleName();

    private final UserRepository repository;
    private final LiveData<List<User>> allUsers;

    public UserViewModel(Application application) {
        super(application);
        repository = new UserRepository(application);
        allUsers = repository.getAllUsers();
    }

    public void insert(User user) {
        repository.insert(user);
    }

    public void update(User user) {
        repository.update(user);
    }

    public void deleteAll() {
        repository.deleteAll();
    }

    public void deleteUser(int id) {
        repository.deleteUser(id);
    }

    public LiveData<List<User>> getAllUsers() {
        return allUsers;
    }
}

> AndroidViewModel를 상속하고 대부분의 메소드들은 델리게이트로 구성한다.



- ViewModel 구현
userViewModel = ViewModelProviders.of(this).get(UserViewModel.class);
userViewModel.getAllUsers().observe(this, new Observer<List<User>>() {
@Override
public void onChanged(List<User> users) {

}
});

> 라이프사이클을 이용해서 ViewModel을 통해 DB 연동을 한다.
> 라이프 사이클 :
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko#declaring_dependencies



3. 샘플코드
소스코드
APK




- Room의 구성을 더 강력하게 만드는 구성은 Room + ViewModel + Paging 조합이다.
추가적으로 Data Binding을 함께 구성하기도 하지만 반드시 필요한 경우만 사용해하 한다.

- Room을 사용하는 주요 목적은 안드로이드 앱 아키텍처 구조 적용과 SQLiteOpenHelper의 불편함을 줄이기 위함이다.

- 안드로이드와 SQLite의 관계를 이해하고 Room 아키텍처를 사용하도록 하자.

동기화 예제 - synchronized

- 자바 동기화(synchronized) 기본 예


1. 문제가 되는 코드
- 기본 코드
public class SimpleSynchronized {

int balance = 100000000;
int count1 = 0;
int count2 = 0;

public int getBalance() {
return balance;
}

public void addBalance(int balance) {
this.balance += balance;
}

public static void main(String[] args) {
final SimpleSynchronized simpleSynchronized = new SimpleSynchronized();
new Thread() {

@Override
public void run() {
while (simpleSynchronized.getBalance() >= 1000) {
simpleSynchronized.addBalance(-1000);
simpleSynchronized.count1++;
}

System.out.println("Thread 1 : " + simpleSynchronized.count1);
}

}.start();

new Thread() {

@Override
public void run() {
while (simpleSynchronized.getBalance() >= 1000) {
simpleSynchronized.addBalance(-1000);
simpleSynchronized.count2++;
}
System.out.println("Thread 2 : " + simpleSynchronized.count2);
}

}.start();

new Thread() {

@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("balance : " + simpleSynchronized.balance);
System.out.println("count total : " + (simpleSynchronized.count1 + simpleSynchronized.count2));
}

}.start();
}
}

- 결과
Thread 2 : 13312
Thread 1 : 89300
balance : -1000
count total : 102612

> 쓰레드1과 쓰레드2가 동일한 시간에 balance 값이 1000 보다 큰 경우만 -1000을 하면 결과적으로 1000 > 0 > -1000이 되는 문제가 발생한다.
> 루프 카운트도 100000000 / 1000 번을 하는 것이 아닌 것이 확인된다.


2. synchronized 적용
- 기본코드
public class SimpleSynchronized {

int balance = 100000000;
int count1 = 0;
int count2 = 0;

public int getBalance() {
return balance;
}

public void addBalance(int balance) {
this.balance += balance;
}

public static void main(String[] args) {
final SimpleSynchronized simpleSynchronized = new SimpleSynchronized();
new Thread() {

@Override
public void run() {
synchronized (simpleSynchronized) {
while (simpleSynchronized.getBalance() >= 1000) {
simpleSynchronized.addBalance(-1000);
simpleSynchronized.count1++;
}
}
System.out.println("Thread 1 : " + simpleSynchronized.count1);
}

}.start();

new Thread() {

@Override
public void run() {
synchronized (simpleSynchronized) {
while (simpleSynchronized.getBalance() >= 1000) {
simpleSynchronized.addBalance(-1000);
simpleSynchronized.count2++;
}
System.out.println("Thread 2 : " + simpleSynchronized.count2);
}
}

}.start();

new Thread() {

@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("balance : " + simpleSynchronized.balance);
System.out.println("count total : " + (simpleSynchronized.count1 + simpleSynchronized.count2));
}

}.start();
}
}

- 결과
Thread 2 : 0
Thread 1 : 100000
balance : 0
count total : 100000

> 쓰레드2는 쓰레드1이 simpleSynchronized 객체를 동기화 하기 때문에 쓰레드1의 작업이 완료되기 전까지 대기 한다.
> synchronized는 동기가 필요한 곳에 적절하게 적용해야 한다.
> 주된 예로 다중쓰레드 환경에서 Map 객체의 put과 remove를 synchronized없이 처리하면 문제가 되는 경우가 있다.





3. 동기화 기본 문법
- 객체 동기화
Object object = new Object();
synchronized (object) {

}

synchronized (this) {

}

> 기본변수(int, long, double 등) 타입은 적용 불가.

- 클래스 단위의 동기화
synchronized (Object.class) {

}

- 메소드 동기화
public synchronized void setObject(Object object) {

}

Restful API 예제코드 - HttpURLConnection

1. Restful API - HttpURLConnection
- 기본코드
String urlStr = "https://www.instagram.com/web/search/topsearch/?context=blended&query=java";
URL url = null;
HttpURLConnection connection = null;
BufferedReader reader = null;

try {
url = new URL(urlStr);
connection = urlStr.startsWith("https://") ? (HttpsURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection();

connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
connection.setRequestProperty("cache-control", "no-cache");

connection.connect();

int code = connection.getResponseCode();
String message = connection.getResponseMessage();

StringBuffer buffer = null;
if (code == HttpURLConnection.HTTP_OK) {
buffer = new StringBuffer();

reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String temp = null;
while ((temp = reader.readLine()) != null) {
buffer.append(temp);
}

reader.close();
}
connection.disconnect();

System.out.println(String.format("Response : %d, %s", code, message));
System.out.println("Response DATA : ");
System.out.println(buffer == null ? "NULL " : buffer.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}


- 결과
Response : 200, OK
Response DATA :
{}


> URL 문자열에서 HTTPS를 판단해서 캐스팅 할 클래스를 선택
> Restful API 사양에 맞는 RequestHeader 설정
> 예제코드에서 사용한 Restful API 주소는 인스타그램 검색 API

2019년 3월 6일 수요일

칼만필터 예제

1. 기본코드
public class Kalman {
    private static final double Q = 0.00001;
    private static final double R = 0.001;

    private double P = 1;
    private double X = 0;
    private double K;


    public Kalman(double value) {
        X = value;
    }

    // 계산
    private void measurementUpdate() {
        K = (P + Q) / (P + Q + R);
        P = R * (P + Q) / (P + Q + R);
    }

    // 현재값을 받아 계산된 공식을 적용하고 반환한다
    public double update(double measurement) {
        measurementUpdate();
        X = X + (measurement - X) * K;
        return X;
    }

}

- 상수 Q와 R의 이해가 필요
- 주 사용처 : 센서(온도, 습도, 자기장, 가속도계), RSSI 시그널 등의 오차 계산
- 칼만필터 위키백과 :
https://ko.wikipedia.org/wiki/%EC%B9%BC%EB%A7%8C_%ED%95%84%ED%84%B0