Android App | ttuburgi

지난 6개월간 프로그래밍을 학습하기 위해 KIC캠퍼스 학원을 다녔습니다. Java를 중심으로 JSP,Spring,Javascript 등 현업에 필요한 웹기술과 Android 앱 개발 을 배울 수 있었습니다. 학원 수료 40여일 전, 팀 프로젝트가 미션으로 주어졌습니다. 저를 포함한 3명의 팀원과 함께 안드로이드 앱을 개발하기로 하였습니다.

개요

프로그래밍 기술을 익히는 것은 언어를 익히는 것과 비슷합니다. 접하는 횟수가 많을 수록 빠르게 익히는 것 같습니다. ttuburgi 는 언제 어디서나 코딩학습을 할 수 있도록 돕는 안드로이드 앱입니다.

특징

  • 틈나는 짧은 시간에 볼 수 있을 만큼의 학습량을 제공합니다.
  • 원하는 프로그래밍 기술을 선택하여 학습할 수 있습니다.
  • 랜덤학습 기능으로 스스로 학습한 내용을 체크를 할 수 있습니다.
  • 북마크, 퀴즈, 학습완료 기능등으로 원하는 학습내용을 관리할 수 있습니다.
  • google maps를 통하여 코딩학습 하기 좋은 공간 정보를 얻을 수 있습니다.

프레젠테이션

시연 동영상

역할

  • 팀내 의견 조율 역할 : 이번 프로젝트에서 저의 역할은 팀 프로젝트를 조율하고 회의를 진행하며, 이슈 및 진행상황 공유의 역할을 했습니다. 특별히 팀장을 정하지 않고 수평적인 체계로 프로젝트를 진행하기로 합의를 하고 프로젝트에 임하였지만 정보가 잘 공유되지 않는 지점이 일어날 경우 놓치지 않고 집고 넘어가려 했습니다.

  • DB 모델링 / 안드로이드 코드작성 : 프로젝트의 DB를 설계하고 코딩하는 작업을 했습니다. Firebase Realtime DB 를 활용하여 클라우드 DB 시스템의 장점을 이용하였고, 정적 데이터를 효율적으로 활용하기 위해 SQLite 를 사용하였습니다.

  • Web app : 앱 관리 사이트로 활용하기 위해 제작하였습니다.

CODE

앱의 모든 코드를 공개할 수는 없지만 필수 기능을 구현하기 위한 코드 일부를 공개하여 어떤 기능이 어떤 코드로 작성되었는지 안내하도록 하겠습니다.

Firebase

Google Firebase 는 앱 개발시 꼭 필요한 서버(사용자 관리, 분석 도구, 데이터베이스, 광고등)의 서비스를 제공하는 BaaS(Backend as a Service) 플랫폼으로서 개발자가 벡엔드 개발 보다는 프론트엔드/비지니스 로직에 집중할 수 있도록 돕는 클라우드 서비스 입니다.

프로젝트에 앱에 파이어베이스를 적용하고 SDK를 추가해야 합니다. 이 부분은 공식문서 에 쉽게 기술되었기에 생략하겠습니다.

이번 프로젝트에서 파이어베이스의 기능 중 사용자관리, 클라우드 DB를 주로 활용하였습니다.

사용자 정보는 회원 가입을 포함해서 앱 전체에 걸쳐서 활용되었습니다.

1
2
3
4
5
6
7
8
9
10
11
< 멤버변수로 선언 >
private FirebaseAuth mAuth;
private FirebaseUser mUser;
< 사용자 프로필을 가져오기 : onCreate() 메서드 내부 >
mAuth = FirebaseAuth.getInstance();
mUser = auth.getCurrentUser();
String userId = null;
if (mUser != null) {
userId = user.getUid();
}

파이어베이스의 Realtime DB 를 사용하였습니다.

app-levelbuild.gradle 파일에 다음 코드를 추가하여 의존합니다.

1
2
3
dependencies {
compile 'com.google.firebase:firebase-database:10.0.1'
}

key:value 형태의 데이터베이스로서 JSON 포맷형태라고 보시면 됩니다.

코드 내부에서 데이터베이스를 활용하기 위해서는 DatabaseReference 인스턴스가 필요합니다.

1
2
3
4
5
6
7
< 멤버변수 선언 >
private DatabaseReference mReference;
< onCreate 메서드 내부 >
if (userId != null) {
mReference = FirebaseDatabase.getInstance().getReference("Users").child(userId).child("language");
}

위 코드를 통해서 데이터베이스에 접근하고 저장, 검색등을 활용할 수 있습니다. 좀 더 구체적인 사용법은 공식 문서 를 참고하시면 됩니다.(아쉽지만 아직 한글번역이 완전히 되어있지는 않네요.)

Google login

파이어베이스에서는 Google, Facebook, Twitter, Github의 사용자 ID를 공유하여 활용할 수 있습니다. 이번 프로젝트에서는 Google 계정으로 로그인을 간단히 할 수 있도록 하였습니다.

app-levelbuild.gradle 파일에 다음 코드를 추가하여 의존합니다.

1
2
3
4
dependencies {
compile 'com.google.firebase:firebase-auth:10.0.1'
compile 'com.google.android.gms:play-services-auth:10.0.1'
}

Google 로그인을 통합하기 위한 코드를 입력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private GoogleApiClient mGoogleApiClient;
< onCreate 메서드 내부 >
//Get Google auth instance
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build();
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this, this)
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.build();
mGoogleApiClient.connect();

버튼 클릭 리스너를 생성합니다.

1
2
3
4
5
6
btn_Google.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
googleSignIn();
}
});

통합 후 결과를 얻기 위한 코드를 입력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final int RC_SIGN_IN = 9001;
private void googleSignIn() {
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
startActivityForResult(signInIntent, RC_SIGN_IN);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == RC_SIGN_IN){
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if(result.isSuccess()){
GoogleSignInAccount account = result.getSignInAccount();
firebaseAuthWithGoogle(account);
}
else
Log.d(TAG, "Google Login Failed");
}
}

로그인 이후 사용자의 상태 변화(로그인/로그아웃)에 대응하는 코드를 입력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private FirebaseAuth mAuth;
private FirebaseAuth.AuthStateListener mAuthListener;
< onCreate() 메서드 내부 >
mAuth = FirebaseAuth.getInstance();
mAuthListener = new FirebaseAuth.AuthStateListener() {
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
user = firebaseAuth.getCurrentUser();
String userId = null;
if (user != null) {
userId = user.getUid();
}
if (userId != null) {
showProgressDialog();
databaseReference = FirebaseDatabase.getInstance().getReference("Users").child(userId);
databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (user.isEmailVerified() && !dataSnapshot.child("language").exists()) {
startActivity(new Intent(MainActivity.this, LangSelect.class));
} else if (user.isEmailVerified() && dataSnapshot.child("language").exists() && !dataSnapshot.child("user_settime").exists()) {
startActivity(new Intent(MainActivity.this, TimeSelectActivity.class));
} else if (user.isEmailVerified() && dataSnapshot.child("language").exists() && dataSnapshot.child("user_settime").exists()) {
startActivity(new Intent(MainActivity.this, NavigationActivity.class));
}
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
} else {
//toastMessage("Signing Out...");
}
}
};
@Override
public void onStart() {
super.onStart();
mGoogleApiClient.connect();
mAuth.addAuthStateListener(mAuthListener);
//FirebaseUser currentUser = mAuth.getCurrentUser();
}
@Override
public void onStop() {
super.onStop();
if (mAuthListener != null) {
mGoogleApiClient.disconnect();
mAuth.removeAuthStateListener(mAuthListener);
}
}

정상적인 로그인 이후 GoogleSignInAccount 개체에서 ID토큰을 가져와 파이어베이스 사용자 인증정보로 교환하고 이 정보로 인증합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final String TAG = MainActivity.class.getName();
private void firebaseAuthWithGoogle(GoogleSignInAccount acct){
showProgressDialog();
AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
mAuth.signInWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success");
//FirebaseUser user = mAuth.getCurrentUser();
makeDate();
//updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.getException());
Toast.makeText(MainActivity.this, "로그인에 실패하셨습니다.",
Toast.LENGTH_SHORT).show();
}
}
});
}

로그아웃은 앱 내부의 세팅 화면에서 기능하도록 하였고, 작성 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
private Button mLogoutButton;
private CustomDialog mCustomDialog;
private FirebaseAuth.AuthStateListener mAuthLisener;
private GoogleApiClient mGoogleApiClient;
private FirebaseAuth mAuth;
< onCeateView 메서드 내부 : Fragment 사용 >
auth = FirebaseAuth.getInstance();
// 로그 아웃을 위한 버튼 이벤트
logoutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 다이얼로그 띄우기
customDialog = new CustomDialog(getActivity(), "로그아웃 하시겠습니까?", "확인을 누르면 로그아웃 후\n로그인 페이지로 이동합니다.", leftListener, rightListener2);
customDialog.show();
}
});
// 로그아웃 다이얼 로그 내에 있는 확인 버튼
public View.OnClickListener rightListener2 = new View.OnClickListener() {
@Override
public void onClick(View v) {
FirebaseAuth.getInstance().signOut();
Auth.GoogleSignInApi.signOut(mGoogleApiClient);
startActivity(new Intent(getActivity().getApplicationContext(), MainActivity.class));
}
};
// 사용자 상태 변화
@Override
public void onStart() {
super.onStart();
mGoogleApiClient.connect();
auth.addAuthStateListener(mAuthLisener);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mGoogleApiClient.isConnected())
mGoogleApiClient.disconnect();
}
@Override
public void onPause() {
super.onPause();
mGoogleApiClient.stopAutoManage((FragmentActivity) getActivity());
mGoogleApiClient.disconnect();
}
@Override
public void onStop() {
super.onStop();
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
mGoogleApiClient.stopAutoManage((FragmentActivity) getActivity());
mGoogleApiClient.disconnect();
}
}
// 로그인 과정의 흐름 처럼 사용자 정보 교환 후 결과 얻기
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SIGN_IN) {
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if (result.isSuccess()) {
GoogleSignInAccount account = result.getSignInAccount();
firebaseAuthWithGoogle(account);
} else {}
}
}
private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
auth.signInWithCredential(credential)
.addOnCompleteListener((Executor) this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
Log.d(TAG, "signInWithCredential:success");
FirebaseUser user = auth.getCurrentUser();
} else {
Log.w(TAG, "signInWithCredential:failure", task.getException());
}
}
});
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {}

코드 작성 후 로그인을 하면 다음과 같이 앱에 사용자로 등록 됩니다. 구글 표시가 되어있네요.

Google 사용자 계정을 활용한 간단 로그인 방법을 구현했습니다. 공식 문서에 설명이 잘 나왔고 부족한 부분은 유투브나 검색을 통해서 참고하였습니다.

회원가입시 이메일 인증

일반적인 이메일과 비밀번호를 통한 가입은 사용자 확인, 보안을 강화하기 위해 이메일로 인증하는 시스템을 만들었습니다.

사용자에게 가입 확인 이메일을 보내는 메서드 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void sendEmailVerification() {
// Send verification email
// [START send_email_verification]
final FirebaseUser user = mAuth.getCurrentUser();
if (user != null) {
user.sendEmailVerification()
.addOnCompleteListener(this, new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
Toast.makeText(JoinActivity.this,
user.getEmail() + "로 인증 메일이 전송되었습니다. ",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(JoinActivity.this,
"인증 메일 전송에 실패하였습니다.",
Toast.LENGTH_SHORT).show();
}
}
});
}
}

이메일이 도착했습니다. 이메일 내용의 URL을 클릭하면 됩니다. (이메일 템플릿은 파이어베이스 콘솔에서 맞춤 설정이 가능합니다. 참고)

계정을 생성 하는 메서드 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void CallSignUp(String email,String password) {
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
Log.d("TESTING", "Sign up Successful" + task.isSuccessful());
if (!task.isSuccessful()) {
Toast.makeText(JoinActivity.this, String.format("회원 가입에 실패하였습니다.%s", task.getException()), Toast.LENGTH_SHORT).show();
} else {
customDialog = new CustomDialog(JoinActivity.this, "인증메일이 발송됐습니다.", "인증메일 확인 후 로그인 해주세요.", rightListener);
customDialog.show();
}
}
});
}

Google maps

코딩하기 좋은 공간정보를 제공하기 위해 Google maps API를 활용하여 지도 서비스를 제공하였습니다.

AndroidManifest.xml 파일의 <application> 하위 요소로 API키를 입력해줍니다.

1
2
3
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value= API키 />

퍼미션 설정을 통해서 필요한 권한을 획득합니다.

1
2
3
4
5
6
7
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

app-levelbuild.gradle 파일에 다음 코드를 추가하여 의존합니다.

1
2
compile 'com.google.android.gms:play-services-maps:10.2.1'
compile 'com.google.android.gms:play-services-location:10.2.1'

해당 프래그먼트 레이아웃에 다음 내용을 추가해줍니다.

1
android:name="com.google.android.gms.maps.MapFragment"

맵을 로딩하기 위해서는 OnMapReadyCallback 인터페이스의 onMapReady() 메서드를 구현해줘야 합니다. GoogleMap 객체의 인자를 받으면서 호출 됩니다.
맵마커 정보(위치, 설명 등)를 설정해주었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public void onMapReady(GoogleMap map) {
mGoogleMap = map;
spreadMarker();
mGoogleMap.setOnInfoWindowClickListener(this);
mGoogleMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
@Override
public View getInfoWindow(final Marker marker) {
LayoutInflater infoInflater = LayoutInflater.from(getActivity());
View myContentsView = infoInflater.inflate(R.layout.info_window, null);
myContentsView.setBackgroundColor(getResources().getColor(R.color.darkGray));
TextView tvLocality = (TextView) myContentsView.findViewById(R.id.tv_locality);
TextView tvConsent = (TextView) myContentsView.findViewById(R.id.tv_consent);
TextView tvInfo = (TextView) myContentsView.findViewById(R.id.tv_info);*/
tvLocality.setText(marker.getTitle());
mGoogleMap.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
String infoUri = "https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query=" + marker.getTitle();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(infoUri)));
}
});
return myContentsView;
}
@Override
public View getInfoContents(Marker arg0) { return null; }
});
//런타임 퍼미션 요청 대화상자나 GPS 활성 요청 대화상자 보이기전에
//지도의 초기위치를 서울로 이동
setCurrentLocation(null, "위치정보 가져올 수 없음",
"위치 퍼미션과 GPS 활성 요부 확인하세요");
mGoogleMap.getUiSettings().setCompassEnabled(true);
// Zoom in, animating the camera.
mGoogleMap.animateCamera(CameraUpdateFactory.zoomIn());
// Zoom out to zoom level 10, animating with a duration of 2 seconds.
mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(10), 2000, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//API 23 이상이면 런타임 퍼미션 처리 필요
int hasFineLocationPermission = ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION);
if (hasFineLocationPermission == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(mActivity,
new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
} else {
if (mGoogleApiClient == null) {
buildGoogleApiClient();
}
mGoogleMap.setMyLocationEnabled(true);
}
} else {
if (mGoogleApiClient == null) {
buildGoogleApiClient();
}
mGoogleMap.setMyLocationEnabled(true);
}
}

getMapAsync() 메서드를 호출하여 GoogleMap 객체가 준비 되었을 때 실행될 콜백을 등록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Inflate the layout for this fragment
view = inflater.inflate(R.layout.activity_map, container, false);
databaseReference = FirebaseDatabase.getInstance().getReference("StudyPlace");
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mActivity = (AppCompatActivity) getActivity();
init();
}
private void init() {
MapFragment mapFragment = (MapFragment) getChildFragmentManager().findFragmentById(R.id.maps);
mapFragment.getMapAsync(this);
}

GPS 서비스 활성화 메서드 입니다.(안드로이드 6.0 버전 이상의 디바이스일 경우에는 퍼미션을 런타임에 요구합니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private void showDialogForLocationServiceSetting() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("위치 서비스 비활성화");
builder.setMessage("위치 서비스가 필요합니다.\n"
+ "위치 설정을 수정 하시겠습니까?");
builder.setCancelable(true);
builder.setPositiveButton("설정", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
Intent callGPSSettingIntent
= new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivityForResult(callGPSSettingIntent, GPS_ENABLE_REQUEST_CODE);
}
});
builder.setNegativeButton("취소", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
builder.create().show();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case GPS_ENABLE_REQUEST_CODE:
// GPS 활성화 시켰는지 체크 합니다.
if (checkLocationServicesStatus()) {
if (checkLocationServicesStatus()) {
if (mGoogleApiClient == null) {
buildGoogleApiClient();
}
if (ActivityCompat.checkSelfPermission(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
mGoogleMap.setMyLocationEnabled(true);
}
return;
}
} else {
setCurrentLocation(null, "위치정보 가져올 수 없음",
"위치 퍼미션과 GPS 활성 여부 확인");
}
break;
}
}

애플리케이션의 지도 정보 서비스에 관해서 작성한 일부 코드를 안내해드렸습니다.

앱 알람 서비스

안드로이드의 Service 기능(백그라운드에서 동작하는 컴포넌트)을 활용하여 사용자가 정해놓은 시간에 학습 알람 서비스를 구현했습니다.

AndroidManifest.xml 파일의 <application> 하위 요소를 입력합니다.

1
2
3
4
<service
android:name=".Alarm.MyService"
android:enabled="true"
android:exported="true" />

사용자가 시간을 지정하면 startService(intent) 메서드의 실행과 동시에 Service 가 실행됩니다.

1
2
3
4
5
6
7
8
public void onDataChange(DataSnapshot dataSnapshot) {
alarm = (String) dataSnapshot.child("user_settime").getValue();
Intent intent = new Intent(TimeSelectActivity.this, MyService.class);
intent.putExtra("alarm", alarm);
startService(intent);
}

Service를 상속받아 클래스를 생성합니다. onStartCommand() 메서드는 Service 의 실행시 호출됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
if(intent != null) {
alarmIntent = intent.getStringExtra(("alarm"));
}
String alarm[] = alarmIntent.split(":");
hour = Integer.parseInt(alarm[0]);
minute = Integer.parseInt(alarm[1]);
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 10);
Intent notificationIntent = new Intent("android.media.action.DISPLAY_NOTIFICATION");
notificationIntent.addCategory("android.intent.category.DEFAULT");
broadcast = PendingIntent.getBroadcast(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), AlarmManager.INTERVAL_DAY, broadcast);
return START_NOT_STICKY;
}

이 서비스를 수행하면 앱을 실행하지 않은 상태에서 지정한 시간에 알림이 옵니다. 알림의 역할은 안드로이드의 컴포넌트인 BroadCastReciver 가 합니다.

AndroidManifest.xml 파일의 <application> 하위 요소를 입력합니다.

1
2
3
4
5
6
<receiver android:name=".Alarm.MyReceiver">
<intent-filter>
<action android:name="android.media.action.DISPLAY_NOTIFICATION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>

BroadcastReceiver 를 상속하는 클래스를 만들고 서비스 내부에서 setRepeating() 메서드가 실행이 되면 onReceive() 메서드가 실행이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void onReceive(Context context, Intent intent) {
Intent notificationIntent = new Intent(context, NavigationActivity.class);
// TaskStackBuilder객체를 통해 addParentStack,
// addNextIntent 메소드를 통해 notificationIntent를 Parent Activity로 갖는
// NavigationActivity로 이동하게 합니다.
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(NavigationActivity.class);
stackBuilder.addNextIntent(notificationIntent);
// FLAG_ACTIVITY_CLEAR_TOP 은 이전 Activity를 새로운 Activity로 지정합니다.
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 만일 이미 생성된 PendingIntent가 존재 한다면, 해당 Intent 의 내용을 변경합니다.
PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = builder
.setTicker("TTUBURGI")
.setContentTitle("TTUBURGI")
.setContentText("학습 할 시간입니다.")
.setAutoCancel(true)
.setSmallIcon(R.drawable.icon_alarm_small)
.setContentIntent(pendingIntent).build();
// 고유 아이디로 알림을 생성
notificationManager.notify(0, notification);
}

알람 서비스가 울리면 다음과 같이 표시가 됩니다.

ServiceBroadcastReceiver 컴포넌트를 통해 학습 알람 서비스를 구현하였습니다.

SQLite 데이터베이스 활용

SQLite 는 안드로이드에 기본으로 포함되어 있는 DBMS 로서 라이브러리 형태로 응용프로그램에 직접 임베디드 하여 사용하는 소프트웨어 입니다.

이번 프로젝트에서 제가 전적으로 담당하였습니다. ttuburgi 에서는 주로 정적데이터 서비스에 활용 되었습니다.

미리 준비한 데이터 베이스 파일을 assets 폴더에 copy 합니다.

DB를 컨트롤 하기 위해 SQLiteOpenHelper 를 상속받는 클래스를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class StudyDBHelper extends SQLiteOpenHelper {
private static String TAG = "StudyDBHelper"; // Lpgcat에 출력할 태그 이름
private static String DB_PATH = ""; // 디바이스 장치에서 데이터베이스의 경로
private static String DB_NAME = "study.db"; // 데이터베이스 이름
private SQLiteDatabase mDatabase;
private final Context mContext;
public StudyDBHelper(Context context) {
super(context, DB_NAME, null, 1); // 1은 데이터베이스 버전
if(android.os.Build.VERSION.SDK_INT >= 17) {
DB_PATH = context.getApplicationInfo().dataDir + "/databases/";
} else {
DB_PATH = "/data/data/" + context.getPackageName() + "/databases/;";
}
this.mContext = context;
}
public void createStudyDatabase() throws IOException {
// 데이터베이스가 없으면 assets 폴더에서 복사해온다
boolean mDataBaseExist = checkStudyDataBase();
if(!mDataBaseExist) {
this.getReadableDatabase();
this.close();
try {
copyStudyDataBase();
Log.e(TAG, "createStudyDatabase database created");
} catch (Exception e) {
throw new Error("ErrorCopyingDataBase");
}
}
}
// /data/data/com.TT.kitcoop.ttuburgi/databases/userstudy.db 가 있는지 확인
private boolean checkStudyDataBase() {
File dbFile = new File(DB_PATH + DB_NAME);
System.out.println(dbFile);
return dbFile.exists();
}
// assets 폴더에서 데이터베이스를 복사한다.
private void copyStudyDataBase() throws IOException {
InputStream mInputStream = mContext.getAssets().open(DB_NAME);
String outFileName = DB_PATH + DB_NAME;
OutputStream mOutputStream = new FileOutputStream(outFileName);
byte[] mBuffer = new byte[1024];
int mLength;
while((mLength = mInputStream.read(mBuffer)) > 0) {
mOutputStream.write(mBuffer, 0, mLength);
}
mOutputStream.flush();
mOutputStream.close();
mInputStream.close();
}
// 데이터베이스를 열어서 쿼리를 쓸 수 있게 만든다
public boolean openStudyDataBase() throws SQLException {
String mPath = DB_PATH + DB_NAME;
mDatabase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.CREATE_IF_NECESSARY);
return mDatabase != null;
}
public synchronized void close() {
if(mDatabase != null)
mDatabase.close();
super.close();
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}

DBadapter 클래스를 생성하여 앱서비스에서 필요한 데이터를 처리하도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public class StudyDataAdapter {
protected static final String TAG = "DataAdapter";
private final Context mContext;
private SQLiteDatabase mDB;
private StudyDBHelper studyDbHelper;
public StudyDataAdapter(Context context) {
this.mContext = context;
studyDbHelper = new StudyDBHelper(mContext);
}
public StudyDataAdapter createStudyDataBase() throws SQLException {
try {
studyDbHelper.createStudyDatabase();
} catch (IOException e) {
Log.e(TAG, e.toString() + " UnableToCreateDatabase");
throw new Error("UnableToCreateDatabase");
}
return this;
}
public StudyDataAdapter openStudyDatabase() throws SQLException {
try {
studyDbHelper.openStudyDataBase();
studyDbHelper.close();
mDB = studyDbHelper.getReadableDatabase();
} catch (SQLException e) {
Log.e(TAG, "open >>" + e.toString());
throw e;
}
return this;
}
public void closeStudy() {
studyDbHelper.close();
}
// 랜덤한 학습창을 보여주기 위한 메서드
public Cursor getRandomList(String subject) {
Cursor randomCursor = null;
try {
// 넘어온 값을 인자로 테이블행 갯수 메서드를 count로 담는다
int count = getCount(subject);
// 인덱스 수 안에서 랜덤 수 추출
Random random = new Random();
int randomId = random.nextInt(count) + 1;
String randomSql = "select * from " + subject + " where id=" + randomId;
randomCursor = mDB.rawQuery(randomSql, null);
if (randomCursor != null) {
randomCursor.moveToNext();
}
} catch (SQLException e) {
Log.e(TAG, "getRandomList >>" + e.toString());
throw e;
}
return randomCursor;
}
// 선택한 테이블 행 갯수를 구하기 위한 메서드
public int getCount(String subject) {
// 테이블 행 갯수 저장을 위한 변수 선언
int count = 0;
// 받아온 값으로 테이블 전체 조회
try {
String allSql = "select * from " + subject;
Cursor allCursor = mDB.rawQuery(allSql, null);
if (allCursor != null) {
allCursor.moveToNext();
count = allCursor.getCount();
}
} catch (SQLException e) {
Log.e(TAG, "getCount >>" + e.toString());
throw e;
}
return count;
}
// 선택한 과목 학습 리스트 구하기 위한 메서드
public Cursor getStudyList(String subject) {
Cursor allCursor = null;
// 받아온 값으로 테이블 전체 조회
try {
String allSql = "select * from " + subject + " order by id asc";
allCursor = mDB.rawQuery(allSql, null);
if (allCursor != null) {
allCursor.moveToNext();
}
} catch (SQLException e) {
Log.e(TAG, "getStudyList >>" + e.toString());
throw e;
}
return allCursor;
}
// 선택한 학습 내용 보여주기 위한 메서드
public Cursor getStudy(String subject, String studyname) {
Cursor studyCursor = null;
try {
String studySql = "select * from " + subject + " where studyname='" + studyname + "'";
studyCursor = mDB.rawQuery(studySql, null);
if (studyCursor != null) {
studyCursor.moveToNext();
}
} catch (SQLException e) {
Log.e(TAG, "getStudy >>" + e.toString());
throw e;
}
return studyCursor;
}
// 코멘트 리스트
public Cursor getCommentList(String subject, String studyname) {
System.out.println(subject);
System.out.println(studyname);
Cursor commentListCursor = null;
try {
String commentListSql = "select * from comment where subject = " + "'" + subject + "'" + " and studyname = '" + studyname + "'";
commentListCursor = mDB.rawQuery(commentListSql, null);
if (commentListCursor != null) {
commentListCursor.moveToNext();
}
} catch (SQLException e) {
Log.e(TAG, "commentListCursor >>" + e.toString());
throw e;
}
return commentListCursor;
}
// 코멘트 추가하기
public void commentInsert(String email, String subject, String studyname, String content) {
mDB = studyDbHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put("email", email);
contentValues.put("subject", subject);
contentValues.put("studyname", studyname);
contentValues.put("content", content);
mDB.insert("comment", null, contentValues);
}
}

로그인 이후 앱에 DB가 임베디드되고 필요한 데이터작업 처리를 위한 준비를 합니다.

1
2
3
4
5
6
7
8
9
10
< 멤버 선언 >
private StudyDataAdapter studyDataAdapter;
< 열기 : onCreateView() 메서드 내부 >
studyDataAdapter = new StudyDataAdapter(getActivity());
studyDataAdapter.createStudyDataBase();
studyDataAdapter.openStudyDatabase();
< 닫기 >
studyDataAdapter.closeStudy();

앱 전반에 걸쳐서 DB를 불러오거나 생성, 수정등에 활용 되었습니다.

Web

파이어베이스는 한 프로젝트에서 여러플랫폼(android, IOS, Web) 을 통합 백엔드로 개발을 할 수 있습니다. Web 은 앱서비스의 관리사이트로 개발하기로 기획하였습니다. 하지만.. 마감전까지 android 앱의 디버깅과 기능 추가에 집중하느라 Web은 마무리 하지 못했습니다.

하지만 레이아웃, Firebase API 구현등은 완성되었고 데이터 통계를 시각화 하는 것을 마무리 하지 못햇습니다. 구현된 부분만이라도 안내해드리겠습니다.

Web 개발은 제가 전적으로 맡아서 진행했습니다.

ttuburgi web 으로 접속하면 됩니다.

파이어베이스의 웹 API 는 Javascript를 통해서 구현합니다. 처음 웹앱을 등록하는 방법은 공식문서 에 잘 나와있습니다.

index.html 파일의 파이어베이스 API(DB, auth) 적용 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<script>
var auth, database, userInfo;
// Initialize Firebase
var config = {
apiKey: ,
authDomain: ,
databaseURL: ,
projectId: ,
storageBucket: ,
messagingSenderId:
};
firebase.initializeApp(config);
auth = firebase.auth();
database = firebase.database();
var authProvider = new firebase.auth.GoogleAuthProvider();
auth.onAuthStateChanged(function(user) {
if ( user ) {
// 인증 성공부
$('#userName').text(user.displayName);
$('#userEmail').text(user.email);
$('#userPhoto').attr('src', user.photoURL);
var admin = "ttubergiapp@gmail.com";
if(user.email !== admin){
alert("관리자만 접근이 가능합니다.");
$(document).click(false);
}else{
}
} else {
// 인증 실패부
auth.signInWithPopup(authProvider);
}
});
</script>

웹의 UI는 안드로이드 앱의 디자인과 일관성 있게 하려했습니다. UI 라이브러리는 Materialize 를 적용하였습니다.

1
2
3
<!--Import materialize.css-->
<link type="text/css" rel="stylesheet" href="./css/materialize.min.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="./css/style.css" />

Materialize 의 보다 더 많은 내용은 공식문서 를 참고하시면 됩니다.


첫 안드로이드 프로젝트의 세부적인 내용을 정리해봤습니다. 국내 자료가 별로 없는 파이어베이스 기술을 선택한 덕에 Google 검색, Youtube, stackoverflow 등 해외 자료를 뒤져가며 프로젝트를 진행하였습니다.

마감 당일까지도 디버깅을 하였지만 아쉬운 부분들이 많습니다. 다른 팀원들도 아쉬움이 많아서 프로젝트를 계속 이어가기로 약속을 하였습니다. 다시 코드를 펼쳐보며 부족한 부분을 채워 나가도록 해야겠습니다.

android app work