본문 바로가기

ANDROID

[졸업프로젝트 개발일지] 안드로이드 심화 - 서버와 협업하는 앱 만들기

실제 개발한 앱의 코드들

졸업프로젝트를 2020년 2학기 - 2021년 1학기, 1년동안 진행하면서 많은것을 배웠다. 앱개발 동아리에서 배운 지식을 기반으로 졸업프로젝트에서 아등바등 구글링하며 개발하는동안 확실히 성장했다고 나는 생각한다!! 특히 기존에는 간단한 안드로이드 앱만 만들 수 있었다면, 졸업프로젝트에서는 서버연결이 필수적이었기 때문에 1)안드로이드앱을 서버와 연결하는 법 2)서버로 단순한 로그인정보를 주고받는 방법 3)동영상과 같은 멀티미디어 파일 주고받는 방법, 이렇게 총3가지를 배웠다.

서버를 구동한 안드로이드 앱 자체를 만들어본 적이 없는데 단순한 이미지도 아니고 동영상을 보내고 받는것을 구현하느라 사실 수많은 버그들과 싸웠었다 ㅎㅎ... 이 블로그 글을 읽는 분들을 그런 시행착오를 간편하게 뛰어넘길 바라는 마음에 서버구동앱을 만드는 방법을 위에서 언급한 3가지의 배움을 기준으로 튜토리얼을 적어보고자한다!

 


 

       앱의 전체적인 플로우 살펴보기       

 

일단 서버연결에 대한 튜토리얼에 앞서, 간단하게 내가 만들었던 졸업프로젝트 앱에 대해 설명하고자 한다!

 

0) 주제설명

일단 졸업프로젝트 주제를 설명하자면, 본 어플리케이션은 abnormal detection이란 딥러닝 모델을 통해 사용자가 업로드하는 블랙박스 영상에서 비정상적인 순간을 찾아 클립으로 제공하는 서비스이다! 블랙박스를 본인 차사고 단서를 잡기위해서도 자주 사용하지만, 범죄 발생시 수사용으로도 공공기관에서 자주 사용하는 모습들을 봐왔다. 그런데 블랙박스는 차사고만을 취급하기때문에 범죄에 대한 단서를 찾을시에는 일일이 찾아봐야해서 시간손실이 컸다. 그래서 우리 어플리케이션은 폭력, 폭발, 강도 등등의 범죄의 순간도 구별해내서 해당 순간을 클립으로 제공해준다. 다만 일개 일반인 사용자에겐 이런 공적인 장점이 크게 와닿지 않으므로 비싼돈주고 블랙박스를 살리가 없다. 그래서 우리는 이름 무료 핸드폰 어플리케이션으로 제작하였다. 그저 다운받고 핸드폰을 자동차에 거치하면 끝! 

 

1) 구성기능

주제는 이와같았고, 그래서 실제로 안드로이드측에서 이 서비스를 어떻게 구동되도록 코드로 구현했는지 설명하려고한다. 기능에 초점을 둬서 설명하자면:

1. 로그인/회원가입 기능

2. 마이페이지 (내 갤러리로 가기, 로그아웃) 기능

3. 촬영하기 기능 (충격감지 기능 탑재)

4. 추출하기 기능 (블랙박스 영상 업로드/ abnormal 순간들 다운로드 기능)

5. 신고영상 만드는 기능

이와 같이 구성되어있다.

 

2) 화면구성

위에서 얘기한 기능들과 위 사진에서 유추할 수 있듯이, 화면 구성은 크게

1. 로그인/회원가입

2. 메인화면

3. 마이페이지 화면

4. 촬영하기 클릭 시 나오는 카메라

5. 갤러리 클릭시 나오는 마이갤러리

6. 추출하기 팝업

이렇게 구성되어있다.


3) 실제 작업물 살펴보기 (링크 유)

 

-코드

siyeonkm/Capstone_Android (github.com)

 

siyeonkm/Capstone_Android

Contribute to siyeonkm/Capstone_Android development by creating an account on GitHub.

github.com

여기가 바로 안드로이드 파트 코드가 올라가있는 깃허브주소이다! (바로 내 깃허브 o((>ω< ))o) 혹시 안드로이드 앱개발 경험이 없다면 파일보는 방법은 이와 같다.

더보기

app >src>main로 가서

java/com/example/capstoneblackbox/ 로 가면 java파일들이 있고,

res>layout에는 xml파일들 (화면), res>drawable에는 멀티미디어 소스파일들이 있다.(png나 xml들)

-UI

https://www.figma.com/file/w5GqofAyknul1czSqVwoFc/Untitled?node-id=0%3A1

 

Figma

Created with Figma

www.figma.com

 


 

 

       코드 뜯어보며 서버 연결 배우기       

 

여기에 작성하는 모든 튜토리얼은 다 100% 본인이 짠 코드임을 알려드립니다! 딱 제가 맡았던 파트만 올립니다 :)

 

0) http통신과 영상 업로드/다운로드를 위한 사전세팅

 

- 사전 설정들

서버와 연결한 어플리케이션을 만들기 위해서는 java파일을 작성하기 전에 넣어야하는 사전 설정들이 있다. 주로 AndroidManifest.xml하고 build.gradle쪽에 적어야한다 (build.gradle도 프로젝트/app으로 두가지 버전이 있는데 여기서는 app만 고치면 된다.)

여기서 AndroidManifest.xml이란 파일은 모든 프로젝트의 루트파일안에 있고, Android 시스템이 앱의 코드를 실행하기 전에 확보해야하는 앱에 대한 필수 정보를 시스템에 제공하는 역할을 한다. build.gradle같은 gradle파일의 경우 빌드배포를 도와주는 기능을 한다! 대충 필요한 api, 플러그인들 설치하고 버전관리를 이 파일에서 한다.

 

AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

- internet에 연결

android.permission.INTERNET을 manifest파일에 적어넣는다. 이것을 추가해놓지 않으면 okhttp를 아무리 적어봐야 권한이 없기때문에 서버에 연결하지 못한다.

 

- 동영상저장과 관련된 코드

동영상을 업로드/다운로드할때 핸드폰 내에 있는 저장소에 접근해야한다. 예시를 들자면, 유저는 MagicBox에서 내 핸드폰 속 카메라 폴더에 있는 영상을 찾아서 업로드하고, 딥러닝처리된 영상을 다운받아서 다운로드 폴더에 저장할것이다. 하지만 만약 READ_EXTERNAL_STORAGE와 WRITE_EXTERNAL_STORAGE가 없다면 아무리 많은 영상이 폴더에 있어도MagicBox는 그것을 가져올 수 없다.

 

-  안드로이드폰에서 저장소가 어떻게 나눠지는지

안드로이드폰에서는 저장소가 크게 2개로 나뉜다. 앱들이 각자 가지고 있는 '내부'저장소와 모든 어플리케이션들이 공통적으로 사용하는 '외부'저장소로 나뉜다. 내부저장소는 해당 앱만 읽고 쓸 수 있고, 어플리케이션을 지우면 같이 사라진다. 그에반해 외부저장소는 모든사람들이 읽을 수 있다. 내가 개발하는 '앱'의 입장에서는 외부저장소는 나만의 저장소가 아니기 때문에 여기다가 읽고쓰려면 권한이 필요하고, 그게 바로 위 코드이다. 굳이 외부저장소를 선택한 이유는 사용자들이 갤러리에서 쉽게쉽게 다운로드 받은 영상들을 확인할 수 있기 바라서 그렇게 설계하였다.

 

build.gradle(app)

implementation 'com.squareup.okhttp3:okhttp:3.3.0'

- 위 코드는 okhttp 3버전을 설치하기 위한 코드이다. 혹시 okhttp가 무엇인지 궁금하다면, http통신을 도와주는 라이브러리이다. 이것을 설치해야 통신이 가능하며 rest api또한 사용할 수 있다.

 


 

1) 서버구동의 중심, ConnectServer.java파일!

이 파일이 우리 앱의 서버구동을 위한 모든 함수를 가지고 있다! 실제 서비스로 런칭할건 아니기 때문에 회원탈퇴나 영상삭제같은 기능은 없어서 모조리 post하고 결과 받아오는류기는 하지만 이 간단한 post기능만으로도 많은 서비스를 구현할 수 있다는점!

 

살펴보기에 앞서서, okhttp(rest api)가 무엇인지 간단히 설명하자면, 서버와의 통신을 도와주는 라이브러리이다. okhttp에는 크게 GET, POST, DELETE 메소드가 있고, GET의 경우 서버에게 특정 데이터를 요청하는 메소드이다. POST의 경우 서버에 데이터를 보내는 메소드이고, DELETE는 서버에 올라가있는 데이터의 삭제를 요청하는 메소드이다. 이 3가지 메소드를 사용해서 서버에게 원하는것을 보내고 받는것이다. 자세한 설명은 코드를 직접 보면서 설명하겠다.


ConnectServer.java

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

- 귀찮으면 그냥 import okhttp3.*;이렇게 필요한 라이브러리를 가져와도 되지만, 필요한것들은 위에 코드와 같다. 대부분 통신하기위한 기본적인 라이브러리들이고, 동영상 주고받는데 필요한 특수 라이브러리같은 경우는 MultipartBody정도인거같다!


 public final MyCookieJar myCookieJar = new MyCookieJar();
    public final OkHttpClient client = new OkHttpClient.Builder().cookieJar(myCookieJar)
            .connectTimeout(30, TimeUnit.MINUTES)
            .readTimeout(30, TimeUnit.MINUTES)
            .writeTimeout(30, TimeUnit.MINUTES).build();

- OkHttpClient의 선언

서버 통신을 하는 새 클라이언트가 여기있어요~!하고 선언해주는 부분이다. (서버통신에서 일반적인 사용자를 client, 서버 그자체를 host로 부른다는것을 알고있자) 쉽게 이해하자면 그냥 서버에 동영상을 올리고 싶어하는 유저 1명을 탄생시켰다고 생각하자!

 

- MyCookieJar

내가 이걸 빼먹어서 로그인구현 시 엄청 고생을 했다...엄청...핸드폰에서든 컴퓨터에서든 인터넷을 사용해본 사람이라면 검색 기록 삭제를 해본적이 있을것이다. 그때 쿠키삭제라는 항목을 본적이 있을것이다! 그 쿠키가 위 코드에서 나오는 쿠키인데, 한마디로 세션을 유지해주는 기능을 제공한다. 즉, 쿠키는 한 유저가 남겨놓은 발자취를 그대로 유지해주는 역할을 한다.

쿠키를 관리하지 않는다면 큰일이 난다. 예를 들면 로그인을 했어도 로그인화면에서 메인화면으로 넘어가는 순간 나에 대한 정보는 모두 증발해버려서 마이페이지도 들어가지 못한다. 로그인하면 알아서 되는건줄 알았는데 사실 로그인을 하는 순간 쿠키에 내 정보를 저장해놔서 로그아웃하기 전까지 나라는 유저를 알아차리는것이었다! (나도 이번에 개발해보면서 알게됐다.) 

위 코드에서 MyCookieJar는 이러한 쿠키를 쌓아놓는 쿠키저장소다. (말그대로 쿠키를 담는 통인거다.) 그래서처음에 MyCookieJar를 생성하고, 우리가 방금 탄생시킨 클라이언트에게 쿠키하나를 물려주는것이다! 로그아웃하기전까지 먹지말라고하면서 :D 클라이언트에게 쿠키를 부여하는 코드는 OkHttpClient.Builder().cookieJar(myCookieJar)이다. 

 

- cookieJar(myCookiJar)뒤로 붙은 온갖 timeout들

이 클라이언트란 친구는 성질이 급해서 기본적으로 1분이 지나면 가버린다. 즉, timeout을 따로 이렇게 설정해주지 않으면 서버에게 통신을 요청하고, 서버로부터 1분 이내에 답변이 돌아오지 않으면 통신실패라고 판정해버린다. 실제로 서버와 데이터가 잘 주고받아지는중이었는데 1분만에 통신실패라고 가정하면 불편하므로 본인이 필요한 만큼 .timeout으로 시간을 설정해준다!


public void requestPost(String url, String video, String path, String size, String date, int user_id) {

        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("full_video", "abnorm.mp4",
                        RequestBody.create(MediaType.parse("video/mp4"), new File(video)))
                .addFormDataPart("date", date)
                .addFormDataPart("size", size)
                .addFormDataPart("storage_path", path)
                .addFormDataPart("user_id",Integer.toString(user_id)).build();

        //작성한 Request Body와 데이터를 보낼 url을 Request에 붙임
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();

        Call call = client.newCall(request);

        //request를 Client에 세팅하고 Server로 부터 온 Response를 처리할 Callback 작성
        call.enqueue(new Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {
                Log.d("ERROR", "Connect Server Error is " + e.toString());
            }

            @Override
            public void onResponse(okhttp3.Call call, Response response) throws IOException {
                String vidname = response.body().string();
                Log.d("MESSAGE", "Response Body is " + vidname);
                Handler mHandler = new Handler(Looper.getMainLooper());
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // 사용하고자 하는 코드
                        Toast.makeText(MainActivity.mcontext, vidname + " 업로드 완료!", Toast.LENGTH_LONG).show();
                    }
                }, 0);
                ((AbnormUploadActivity)AbnormUploadActivity.abupcontext).fromAbUptoHomeActivity();
            }
        });
    }

마지막으로 실제 POST 메소드를 살펴보자. 위 코드는 서버에게 영상을 보내고싶다고 요청하는 POST함수이다. 너무 세세하게 들여다보면 한나절이 걸리므로 핵심만 뽑아서 설명하겠다. 

 

- RequestBody

이부분은 GET이나 DELETE에는 없는 POST만의 부분이다. POST의 경우 다른애들과 다르게 서버에게 "무언가"를 보낸

다. 그렇기에 요청을 보낼때 보내고싶은 이 "무언가"도 같이 동봉해서 보내야하는데, 그걸 RequestBody안에 넣어서 보

낸다! 만드는 방법은 1) RequestBody를 생성하고 2) .add~~()로 보내고싶은 데이터를 붙이고 3) .build()로 마무리한다.

 

- Request

편지를 보낼때 우리는 어떻게 할까? 그냥 우체국가서 "이거 친구한테 보내주세요"한다고 보내지지않는다. 보내고 싶은

편지를 편지지에 넣고, 편지지에 "주소"를 적는다. 그리고 우체국에 가서 요청을 한다. 서버통신도 마찬가지이다.

1)Request를 생성하고 2) .url()로 서버주소를 적고 3) .post(requestbody)로 원하는 데이터를 넣고 4) .build()로 마무리한다. 

 

- Call

RequestBody가 편지를 쓰는거고 Request가 편지봉투에 편지지넣고 주소를 쓰는거였다면 Call을 우체국에 가서 진짜로 부치는 행위이다. 새로운 Call을 생성해서 new Call(request)하면 이제 내 요청이 서버로 날아가게 된다.

 

- Callback()

이 부분은 call.enqueue()의 파라미터부분이다. 이미 내장되어있는 메소드인데 내가 오버라이딩하는 부분이다! 

서버에 요청을 보낸 후 어떤상황인지에 따라 어떻게 행동할지를 규정하는 메소드를 내가 필요한대로 바꾸면 된다. 

onFailure같은 경우는 서버와 접선하는데 실패한 경우다. 이때는 주로 에러가났음을 화면에 띄워서 유저에게 알려주는것이 좋다.

onResponse의 경우는 서버와 접선하는데 성공한 경우다. 이때는 주로 서버가 보내온 데이터를 가져와서 사용한다든가, 역시 유저에게 완료됐음을 알려주면 된다.

 


 

2) 로그인/회원가입 - MainActivity.java, SigninActivity.java

 

기본적인 okhttp 구동방식을 알게됐으니까 개별적인 서비스에서 어떻게 사용하는지 보자. 


ConnectServer.java

public void requestPost(String url, final String id, final String password){

        //Request Body에 서버에 보낼 데이터 작성
        RequestBody requestBody = new FormBody.Builder().add("email", id)
                .add("password", password).build();

        //작성한 Request Body와 데이터를 보낼 url을 Request에 붙임
        Request request = new Request.Builder().url(url).post(requestBody).build();

        //request를 Client에 세팅하고 Server로 부터 온 Response를 처리할 Callback 작성
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {
                Log.d("error", "Connect Server Error is " + e.toString());
                Toast.makeText(MainActivity.mcontext, "서버 오류", Toast.LENGTH_LONG).show();
            }

            @Override
            public void onResponse(okhttp3.Call call, Response response) throws IOException {
                String res = response.body().string();
                Log.d("aaaa", "Response Body is " + res);
                if(res.length()<5) {
                    user_id = "0" + res.substring(0, res.length()-1);
                    ((MainActivity)MainActivity.mcontext).goHomeActivity();

                }
                else{
                    Handler mHandler = new Handler(Looper.getMainLooper());
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // 사용하고자 하는 코드
                            Toast.makeText(MainActivity.mcontext, "없는 아이디입니다", Toast.LENGTH_LONG).show();
                        }
                    }, 0);

                }
            }
        });
    }

-설명

위는 ConnectServer.java에서 로그인시 필요한 서버 POST 메소드이다. requestPost라고 이름 지었고, 간단히 기능을 설명하면 파라미터로 받아온 id와 pw를 서버에게 넘긴다. 그러면 서버는 서버내에서 유저에게 매겨놓은 user_id를 넘겨준다.(int형임)

 

-response.body()

서버가 보내온 응답의 내용이 여기에 들어있다. 나는 서버개발자분과 협업하므로 이 응답이 string형인걸 안다. 그러므로 response.body().string()으로 받아와서 저장해놓는다. 저장하는이유는, 나중에 영상 다운로드 받을때 "본인"이 올렸던 영상에 딥러닝 처리한 영상들만 받아야하니까 이때 user_id를 서버에게 보내줘야한다. 그래서 ConnectServer내에 변수하나를 선언해서 받아온 response를 저장하자!

만약 응답이 제대로 와서 응답 내용을 뜯어봤는데 user_id를 보내준게 아니라면 모종의 오류가 났거나 없는 아이디여서 문제가 생긴것일거다. 그런경우에는 장문의 http파일 내용이 주르륵 오기때문에 그냥 간단하게 string길이가 5보다 크면 없는 아이디 오류가 생긴것으로 간주했다.

 


MainActivity.java

public final ConnectServer connectServerPost = new ConnectServer();

btnlogin.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                id = txtId.getText().toString();
                pw = txtPw.getText().toString();

               if(id.compareTo("") == 0 || pw.compareTo("") == 0){
                    Toast.makeText(mcontext, "빈칸을 채워주세요", Toast.LENGTH_SHORT).show();
                }
               else {
                   connectServerPost.requestPost("http://3.34.148.201/login", id, pw);
               }
            }
        });

- ConnectServer에서 만들었던 함수 호출

아까 보여주었던 requestPost를 화면과 연결된 Activity파일에서 호출을 해야 유저의 동작에 따라 작동할것이다. 위 Activity파일은 로그인 화면과 연결되어있다. 그래서 onClick, 즉 로그인버튼을 클릭하면 유저가 입력한 string을 가져와서 id와 pw로 저장하고, 빈칸이 아닌이상 requestPost를 호출해서 서버에게 해당 id와 pw를 넘겨준다. 참고로 맨 첫번째 파라미터는 서버주소이다!

 


SigninAcitivity.java

btnsignin.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                id = txtId.getText().toString();
                pw = txtPw.getText().toString();

                if(id.compareTo("") == 0 || pw.compareTo("") == 0){
                    Toast.makeText(scontext, "빈칸을 채워주세요", Toast.LENGTH_SHORT).show();
                }
                else {
                    //TODO: id와 pw를 db에서 찾아서 >> 없으면 goHomeActivity

                    ((MainActivity)MainActivity.mcontext).connectServerPost
                            .requestSameId("http://3.34.148.201/api/user", id, pw);
                    //TODO: >> 있으면 토스트 발생 & 안넘어감
                    //Toast.makeText(mcontext, "입력하신 아이디/패스워드가 이미 존재합니다", Toast.LENGTH_LONG).show();
                }
            }
        });
       

이건 회원가입 Activity.java파일이다. 로그인과 구동방식은 매우 비슷하나, ConnectServer내의 다른 함수를 사용한다. 거의 비슷한데 왜 따로 만들었냐면, 로그인의 경우 db내에 유저가 보내온 id가 있어야 성공적인것인데 반해 회원가입의 경우 db내에 id가 있으면 중복이므로 없는 id를 보내와야한다. 서로 상반되는것을 요구하기 때문에 requestSameId라는 함수를 따로 만들었다. 구성은 100% 똑같다.

 


ConnectServer.java - requestSameId

 public void requestSameId(String url, final String id, final String password){

        //Request Body에 서버에 보낼 데이터 작성
        RequestBody requestBody = new FormBody.Builder().add("email", id)
                .add("password", password).build();

        //작성한 Request Body와 데이터를 보낼 url을 Request에 붙임
        Request request = new Request.Builder().url(url).post(requestBody).build();

        //request를 Client에 세팅하고 Server로 부터 온 Response를 처리할 Callback 작성
        client.newCall(request).enqueue(new Callback() 
        {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) 
            {
                Log.d("error", "Connect Server Error is " + e.toString());
                Toast.makeText(MainActivity.mcontext, "서버 오류", Toast.LENGTH_LONG).show();
            }

            @Override
            public void onResponse(okhttp3.Call call, Response response) throws IOException 
            {
                String res = response.body().string();
                Log.d("aaaa", "Response Body is " + res);
                if(res.equals("ok")) ((SigninActivity)SigninActivity.scontext).goMainActivity();
            }
        });
    }

 


 

3) 영상 업로드/다운로드 - Popup2Activity.java, MyAlbum func, AbnormActivity.java

 

다음은 우리 어플리케이션에서 가장 중요한 기능, 영상 업로드와 다운로드다!!! 이부분이 역시 가장 어려웠었다. 흔히 서버구동하는 앱을 만들때 String이나 Json 데이터형을 주고받고, 좀더 어려운걸 한다고 해봐야 이미지를 주고받는데에 끝나는 경우가 많다. 실제로 레퍼런스 코드를 찾으러 헤매고있을때도 멀티미디어파일 중 이미지만이 있었고, 동영상 관련 코드는 정말 찾기 어려웠다. 시행착오도 많았지만 무사히 코드를 짜서 다행이다! 혹시 동영상을 서버에 업로드하는 방법을 찾던 사람이라면 이블로그에 온걸 환영한다  :D 그럼 코드를 살펴보자!

 


Popup2Activity.java - onCreate

btnup.setOnClickListener(new View.OnClickListener(){
	@Override
	public void onClick(View view) {
		goToAlbum();
	}
});

btndwn.setOnClickListener(new View.OnClickListener(){
	@Override
	public void onClick(View view) {
		((MainActivity)MainActivity.mcontext).connectServerPost
		.requestVideoCnt("http://3.34.148.201/api/edited/count");
	}
});

-설명

이번에는 setOnClickListner가 두개다! Popup2Activity.java는 위에서 보여줬던 화면중에 추출하기 화면에 해당한다. 메인화면에서 사용자가 "추출하기 버튼"을 클릭하면 팝업이 뜨고, 거기에는 1)업로드 2)다운로드 버튼이 있다. 관리하기 쉽게 일부로 업로드하는 코드와 다운로드하는 코드를 분리해놓은것이다 :D

 

- 업로드

영상을 서버에 업로드하려면 폰안에 있는 동영상 중 하나를 골라서 보내야할것이다. 사용자가 선택할 수 있게금 갤러리를 띄워야할텐데, 그것이 바로  goToAlbum() 함수이다.

 

-다운로드

다운로드할때는 그저 서버로부터 영상을 다운받으면되지만, 서버개발자분께서 만드신 서버 url 구조를 보면 한 url에서 모든 영상을 다운받을 수 있는것이 아니라 영상마다 각자의 url이 있다. 예를들어 0701.mp4의 url은 "http://3.34.148.201/api/edited/0701.mp4"이다. 그러므로 나는 mp4파일의 이름을 알아야 url을 만들어서 다운로드를 하는것이 가능했다. 그래서 서버개발자분께서 영상 이름을 알아낼 수 있는 서버주소를 만들어주셨다. ~~/edited/count인데 한마디로 abnormal detection으로 나온 추출된 영상들 (edited video)의 count이다. 사실 영상이름들이 user_id와 video_id, edited_id로 구성되어있기때문에 일련의 숫자만 서버에서 보내주면 (해당 유저의 일반영상개수, 추출된 영상 개수 등) 내가 조합해서 url을 구성할 수 있었다.

어쨋든, 다운로드 버튼을 클릭하면, ConnectServer.java에 있는 requestVideoCnt 함수를 호출해서 추출된 영상이름을 알아낼 수 있는 서버url로 접속해서 알아오는 방식이다.

 


Popup2Activity.java - goToAlbum(), onActivityResult()

private void goToAlbum() {
  Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
  intent.setType("video/*");
  startActivityForResult(intent, PICK_FROM_ALBUM);
}


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == PICK_FROM_ALBUM) {
    Uri uri2;
    try {
      uri2 = data.getData();
      if (uri2.toString().contains("video")) {
      UriToPath uri2path = new UriToPath();
      videopath = uri2path.getPath(p2context, uri2);
      popup_to_abup();
      }
    } 
    catch (Exception e) {
      Intent intent = new Intent(Popup2Activity.this, HomeActivity.class);
      intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
      startActivity(intent);
    }
  }
}

- goToAlbum()

위 코드가 바로 업로드버튼을 클릭했을시 작동되는 goToAlbum() 함수이다. 한마디로 갤러리앱을 현재 우리앱 위에 띄워주는것이다. Intent를 생성하고, "외부저장소의 동영상파일"을 "선택"하는 액티비티를 실행한다. 여기서 Intent는 뭐고 Activity는 뭐지?라고 궁금할 수도 있다. 안드로이드앱은 모두 Activity에 의해서 돌아가고, 액티비티에서 다른 액티비티를 실행할 때, intent를 통해 이루어진다. Activity는 사용자에게 보여지는 액션을 얘기한다. 뒷단에서 돌아가는 데이터베이스말고, 버튼을 누르면 다음화면이 나오고, 글자를 입력하는 그런활동들을 이야기한다. 

그래서 goToAlbum이란 함수는 외부저장소 속 동영상들만 골라서 화면에 보여주며, 사용자가 동영상을 누르면, 그것을 선택되었다고 판단하여 영상정보를 가져오는 activity를 실행시키는 함수이다!

 

-onActivityResult()

이 함수는 goToAlbum으로 실행된 새로운 activity(영상고르는거)의 결과물을 처리하는 함수이다. activity가 끝나고 나면 사용자가 선택한 동영상의 uri(저장위치)를 리턴한다. 이를 data.getData()로 가져와 uri 데이터형 변수에 저장하고, 이를 절대경로로 바꿔준다! 

uri와 절대경로 모두 동영상의 위치를 가리키지만 uri는 뭐랄까...상대적인 경로라고 표현해도되는지 모르겠지만 정확한 절대경로가 아니다! 그래서 uri로 영상을 찾을라하면 안찾아진다...그러므로 절대경로로 바꾸자.

근데 절대경로로 바꾸는것도 따로 함수를 만들어야한다...그래서 절대경로로 바꿔주는 함수도 첨부하지만 시간상 설명은 하지 않겠다. 

어쨋든, 받아온 uri를 절대경로로 바꾸고, 이를 현재 java파일 속 public 변수에 저장해놓는다. 그리고 완료하면 진짜 영상 업로드를 하는 activity로 이동한다. 이 activity는 AbnormalUploadActivity.java파일로 따로 만들어놨고 별건없고 그냥 액티비티가 onCreate되자마자 서버의 requestPost() 함수를 호출한다. 그러므로 이 java파일에 대한 설명은 생략하고, requestPost() 함수 설명으로 넘어가겠다.

 


ConnectServer.java - requestPost()

public void requestPost(String url, String video, String path, String size, String date, int user_id) {

        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("full_video", "abnorm.mp4",
                        RequestBody.create(MediaType.parse("video/mp4"), new File(video)))
                .addFormDataPart("date", date)
                .addFormDataPart("size", size)
                .addFormDataPart("storage_path", path)
                .addFormDataPart("user_id",Integer.toString(user_id)).build();

        //작성한 Request Body와 데이터를 보낼 url을 Request에 붙임
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();

        Call call = client.newCall(request);

        //request를 Client에 세팅하고 Server로 부터 온 Response를 처리할 Callback 작성
        call.enqueue(new Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {
                Log.d("ERROR", "Connect Server Error is " + e.toString());
            }

            @Override
            public void onResponse(okhttp3.Call call, Response response) throws IOException {
                String vidname = response.body().string();
                Log.d("MESSAGE", "Response Body is " + vidname);
                Handler mHandler = new Handler(Looper.getMainLooper());
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // 사용하고자 하는 코드
                        Toast.makeText(MainActivity.mcontext, vidname + " 업로드 완료!", Toast.LENGTH_LONG).show();
                    }
                }, 0);
                ((AbnormUploadActivity)AbnormUploadActivity.abupcontext).fromAbUptoHomeActivity();
            }
        });
    }
   

-설명

위 함수는 post 메소드로, request에 영상파일과 각종 영상정보들을 body에 넣어서 보낸다. 기본적인 okhttp get, post 메소드 구성요소는 설명했으므로 requestBody에 집중해서 설명하겠다.

 

-MultipartBody

동영상과 같은 멀티미디어 파일을 보낼때는 반드시  RequestBody requestBody = new MultipartBody.Builder()를 사용해야한다. 그 이유는 파일을 전송하려면 body형식이 form-data형식이어야하는데, 일반적인 body의 경우는 x-www-form-urlencoded형식이다. 나도 이번이 첫 서버연결이라 정확한건 알지 못하지만, 파일전송시에는 이러한 문제로 인해 MultipartBody를 사용해야한다. 그냥 body를 쓰면 form-data 형식이어야하는데 지금 형식은 x-www어쩌구다 라면서 알차게 빨간로그가 주르륵 뜨는걸 볼 수 있다 ^^....

 

-addFormDataPart(): 일반적인 경우

body에 보내고 싶은 정보를 추가할땐 .addFormDataPart()를 붙여주면된다. 보내고싶은 데이터의 tag를 앞에 String으로 써주고, 실제 data를 두번째 파라미터로 적어주면된다. 이때 tag는 서버 속 데이터 태그랑 똑같은걸 적어야한다는점! 아마 협업할때 서버개발자분이 이 서버url로 "name", "id", "file" 데이터를 보내주세요. "name"은 String형이고...하면서 알려주실것이다. 그대로 보내면된다!

 

-addFormDataPart(): 파일넣기

다른거야 대충 보내면 되는데 영상파일은 딱봐도 .addFormDataPart() 파라미터가 엄청 복잡해보인다. 살펴보자. 첫번째 파라미터는 남들과 같이 tag명을 적는것이다. 그리고 두번째 파라미터는 보낼 파일의 이름을 설정해주는것이다. 실제 동영상이름과 같을필요는 없고 그냥 원하는 이름을 적어주면된다. 세번째 파라미터는 실제파일을 넣는다. RequestBody를 만들고, 그 안에 파일타입(동영상인지 이미지인지 일반파일인지)을 첫 파라미터로, 실제파일을 두번째 파라미터로 넣으면된다. 이때 중요한점! 동영상 "파일"을 넣어야한다. 동영상 "경로" 아니다!!! 동영상의 절대경로를 적어넣으면 알아서 보내지는줄 알고 경로적었다가 몇시간을 삽질했다. 반드시 new File(파일경로) 이렇게 적어주어야하는거 잊지말자.

 


ConnectServer.java - requestVideoCnt()

public void requestVideoCnt(String svurl){
        Request request = new Request.Builder()
                .url(svurl)
                .build();

        client.newCall(request).enqueue(new Callback() {

            //비동기 처리를 위해 Callback 구현
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d("ERROR", "error + Connect Server Error is " + e.toString());
                t = false;
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Headers responseHeaders = response.headers();
                String vidcnt_s = response.body().string();
                Log.d("MESSAGE", "동영상 개수: " + vidcnt_s);

                Arrays.fill(num_of_video, -1);
                for(int i = 0; i<vidcnt_s.length(); i++) {
                    num_of_video[i] = vidcnt_s.charAt(i) - '0';
                }
                ((Popup2Activity)Popup2Activity.p2context).popup_to_ab();
            }
        });
    }
   

-설명

위 함수는 단순히 동영상이름을 알 수 있는 정보를 넘겨주는 서버에 접속해서 정보를 받아오는것이기 때문에 GET형식을 띈다. 그러므로 .body()없이 .url만 request에 넣어서 build하고 바로 client.newCall을 한다. 그렇게 해서 response를 받으면, 받아온 String 데이터를 ( "070001"과 같은 숫자를 표시한 String임) 파싱해서 int형으로 바꿔준다. 그리고 나만의 처리를 해서 동영상을 다운로드 할때 동영상을 다운로드하는 url을 조합할 수 있도록 배열같은곳에 저장해놓는다. 

모든 처리를 다 하고나면 화면은 팝업에서 다운로드 로딩화면으로 넘겨준다.

 


ConnectServer.java - requestVideoGet()

public void requestVideoGet(String svurl)
    {
        for(int i = 1; num_of_video[i] != -1; i++) {
            for(int j = 1; j<num_of_video[i]+1; j++) {
                String vid_name = "edited"+ user_id + "0"+ String.valueOf(i) + "0" + String.valueOf(j) + ".mp4";
                Log.d("MESSAGE", "동영상: " + vid_name);
                String vid_url = svurl + "/" +vid_name;

                Request request = new Request.Builder()
                        .url(vid_url)
                        .build();

                client.newCall(request).enqueue(new Callback() {
                    private File mediaFile;

                    //비동기 처리를 위해 Callback 구현
                    @Override
                    public void onFailure(Call call, IOException e) {
                        Log.d("ERROR", "error + Connect Server Error is " + e.toString());
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        InputStream inputStream = null;
                        inputStream = response.body().byteStream();
                        String err = new String();

                        try {
                            byte[] buff = new byte[1024 * 4];
                            long downloaded = 0;
                            long target = response.body().contentLength();

                            String dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
                                    .getAbsolutePath().toString() + "/MagicBoxAbnormal";
                            File dirFile = new File(dir);

                            //매직박스용 외부저장소 폴더 생성
                            if(!dirFile.exists()) {
                                dirFile.mkdirs();
                            }

                            mediaFile = new File(dir + "/" + "비정상_"+vid_name);

                            if (this.mediaFile.exists()) {
                                this.mediaFile.delete();
                            }


                            OutputStream output = new FileOutputStream(mediaFile);
                            while (true) {
                                int readed = inputStream.read(buff);

                                if (readed == -1) {
                                    break;
                                }
                                output.write(buff, 0, readed);
                                //write buff
                                downloaded += readed;
                            }
                            output.flush();
                            output.close();

                        } catch (IOException e) {
                            e.printStackTrace();
                        }finally{
                            if(inputStream != null){
                                inputStream.close();
                            }
                        }
                        MediaScanner mediaScanner = new MediaScanner(getApplicationContext(), mediaFile);
                        Handler mHandler = new Handler(Looper.getMainLooper());
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                // 사용하고자 하는 코드
                                Toast.makeText(MainActivity.mcontext, "비정상_"+vid_name + "영상 다운로드 완료! 갤러리에서 확인하실 수 있습니다", Toast.LENGTH_SHORT).show();
                            }
                        }, 0);
                    }
                });
            }
        }
    }

-설명

드디어 마지막으로 영상 다운로드 함수이다. 나는 받아야하는 영상개수가 여러개라서 for문으로 돌려가며 다운을 받았다. url 조합하는거는 우리 프로젝트만의 특수케이스니까 설명하지 않는다. 어쨌든, for문을 돌려가며 url을 만들어서 call을 한다! 거기까진 다른것과 똑같고, response를 받고나서 처리과정이 남다르다.

 

-동영상이 어떻게 다운받아지는가?

비트맵을 들어본적이 있는가? 이미지나 동영상 파일들은 태생부터 그냥 이미지!로 컴퓨터속에 있는게 아니다. 모두 다 2차원 비트배열로 만들어진 비트맵으로 구성되어있다. 그러므로 이 함수를 통해 response가 오면 수많은 0101로 구성된 byte가 stream으로 주르륵 들어온다. 이를 우리는 비트맵으로 재구성해서 mp4파일에 작성해주면 정상적인 동영상이 만들어지는것이다!

 

- 코드 상세설명

file을 작성하기 위해서는 inputStream과 outputStream이 있어야한다는건 전공자라면 수업시간에 들었을 것이다. 그러므로 간단하게 step으로 나눠서 설명하려고한다.

1) response로 넘어온 bytestream을 inputstream으로 하고

2) 이 bytestream을 작성할 file을 생성하고, file을 저장할 위치를 정해서 절대경로를 만든다.

(동영상 폴더에 다운받은 이 동영상을 new.mp4로 저장하고싶다면 "Environment.getExternalStoragePublicDirectory

(Environment.DIRECTORY_MOVIES).getAbsolutePath().toString() + "/new.mp4"로 절대경로를 만들어주면된다.)

3) 그리고 한줄씩 inputStream을 읽고, 이를 outputStream에 적는 과정을 while문으로 반복한다.

4) 이대로 끝내도되지만, 그냥 끝내버리면 갤러리에 들어갔을때 나오지않는다!!! 내장메모리에는 멀쩡히 잘 들어가있지만 갤러리에는 보이지 않을 수 있는데, 이때는 핸드폰에게 미디어스캔을 하라고 시켜야 새로생긴 동영상이 있음을 알고 갤러리에서 보이게 해준다. 이를 하려면 MediaScanner를 생성해서 파라미터에 applicationcontext와 내가 보이게하고싶은 파일주소를 적으면된다.

 

이렇게 내가 개발했던 졸업프로젝트를 통해서 서버통신하는 안드로이드앱을 만드는방법을 알아보았다!! 단순히 연결하는방법에 그치지않고 get, post하는 방법부터해서 동영상 주고받는방법까지 정말 방대한 내용이 들어갔는데, 아무쪼록 도움이 됐으면 하는 마음이다.

 


 

 

       참고한 사이트들       

[안드로이드][OkHttp] 파일 다운로드 방법 : 네이버 블로그 (naver.com)

 

[안드로이드][OkHttp] 파일 다운로드 방법

이번 포스트에서는 OkHttp 라이브러리를 이용하여 파일을 다운로드하는 방법을 정리합니다. OkHttp 라...

blog.naver.com

Example of how to download a video using Okhttp and show a progress bar to the user (github.com)

 

Example of how to download a video using Okhttp and show a progress bar to the user

Example of how to download a video using Okhttp and show a progress bar to the user - VideoDownloader.java

gist.github.com