반응형

AsyncTask 예제를 작성하기 위해 프로그램 작성하기 시작했는데 필요한 기능들을 하나씩 계속 넣다보니 여러 가지가 포함되어 버렸습니다.  최종적으로 완성된 앱은 URL 주소로부터 동영상 다운로드 후 재생시켜 주는 작업을 합니다.  포스팅에 포함된 코드에는 다음 내용들이 추가되어있습니다.  주석을 추가해놓긴 했지만 복잡해보여서 추후 다음 항목별로 따로 포스팅을 작성하여 코드 설명을 추가하도록 하겠습니다. 

  • 안드로이드 6.0 마시멜로( API 23 )이상에서 런타임 퍼미션(Runtime Permission) 처리

  • AsyncTask 사용

  • URLConnection, InputStream, OutputStream을 이용하여 URL 주소에 있는 동영상 파일 다운로드 후, 로컬에 저장하기

  • PowerManager.WakeLock를 사용하여 다운로드 중 전원버튼을 누르더라도 CPU가 잠들지 않도록 처리

  • AsyncTask를  사용하여 파일 다운로드 시 ProgressDialog를 이용하여 다운로드 상태 표시 

  • Intent를 사용하여 로컬에 저장된 동영상을 설치되어 있는 플레이어 앱에서 플레이 시키기

  • AlertDialog를 사용하여 사용자에게 물어보기



본 포스팅의 코드를 설명하기 위해 추가로 작성한 포스팅입니다.

[Android/개념 및 예제] - 안드로이드 개념 및 예제 - AsyncTask




앱 실행과정

프로그램 코드



앱 실행과정


안드로이드 6.0 마시멜로( API 23 )에서만 앱 태스트가 이루어졌습니다.


처음 앱을 실행시키면 퍼미션 허용를 요청합니다. 인터넷에서 다운받은 파일을 SD카드에 저장 후, 다시 SD 카드에서 읽어오기 위해선 퍼미션이 필요하기 때문입니다.  


기존에는 AndroidManifests.xml에만 퍼미션을 추가하면 되었지만 안드로이드 6.0 마시멜로( API 23 )부턴  런타임 퍼미션(Runtime Permission)이 추가되어씁니다. 그래서 앱을 처음 실행할 때 추가로 퍼미션 허가 화면을 보여주며 사용자에게 물어봅니다. 


여기서 퍼미션  허용을 거부해보면 


다시 한번 퍼미션이 있어야 앱이 실행가능하다고 물어봅니다. 여기서 아니오를 선택하면 앱을 종료하도록 되고 예를 선택하면 다시한번 퍼미션 허가를 요청하게 됩니다.


이번엔 퍼미션 요청을 허용해봅니다.


이제 앱 초기 화면이 보입니다.  다운로드 버튼을 누르게 되면 


다운로드가 실행되고, 진행상태를 다이어로그로 보여줍니다. 


다운로드 완료시 동영상을 플레이시킬 앱들의 목록이 보입니다. 하나를 선택하면 


선택한 앱에 의해서 동영상이 플레이 됩니다.  동영상 재생 중 뒤로가기를 누르면 


초기 상태로 돌아옵니다. 다시 다운로드를 선택하면 


이미 SD카드에 다운로드 받은 파일이 있다고 다시 받을지 물어봅니다.  예를 선택하면 기존 파일을 삭제하고 다시 다운로드가 시작되며, 아니오를 선택하게 되면 


기존에 다운로드 받았던 동영상 파일을 플레이하기 위한 앱들의 목록이 보입니다. 


선택한 앱에 의해서 동영상이 플레이 되게 됩니다.



프로그램 코드


AndroidManifests.xml

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.tistory.webnautes.asynctask_example">
 
    <!-- 인터넷 연결 허용하는 퍼미션 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- SD카드 기록 허용하는 퍼미션 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- SD카드 읽기 허용하는 퍼미션, 킷캣이후로는 필요없음 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!-- CPU 상태 유지 및 화면 꺼짐 제어를 위한 퍼미션 -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <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>
cs


activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical" >
 
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dip"
        android:text="다운로드" />
 
</LinearLayout>
cs


MainActivity.java

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package com.tistory.webnautes.asynctask_example;
 
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
 
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
 
public class MainActivity extends AppCompatActivity {
 
    private ProgressDialog progressBar;
 
    static final int PERMISSION_REQUEST_CODE = 1;
    String[] PERMISSIONS = {"android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE"};
    private File outputFile; //파일명까지 포함한 경로
    private File path;//디렉토리경로
 
    private boolean hasPermissions(String[] permissions) {
        int res = 0;
        //스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인
        for (String perms : permissions){
            res = checkCallingOrSelfPermission(perms);
            if (!(res == PackageManager.PERMISSION_GRANTED)){
                //퍼미션 허가 안된 경우
                return false;
            }
 
        }
        //퍼미션이 허가된 경우
        return true;
    }
 
 
    private void requestNecessaryPermissions(String[] permissions) {
        //마시멜로( API 23 )이상에서 런타임 퍼미션(Runtime Permission) 요청
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             requestPermissions(permissions, PERMISSION_REQUEST_CODE);
        }
    }
 
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
 
        if (!hasPermissions(PERMISSIONS)) { //퍼미션 허가를 했었는지 여부를 확인
            requestNecessaryPermissions(PERMISSIONS);//퍼미션 허가안되어 있다면 사용자에게 요청
        } else {
                //이미 사용자에게 퍼미션 허가를 받음.
            }
 
        progressBar=new ProgressDialog(MainActivity.this);
        progressBar.setMessage("다운로드중");
        progressBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressBar.setIndeterminate(true);
        progressBar.setCancelable(true);
 
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) { //1
                //웹브라우저에 아래 링크를 입력하면 Alight.avi 파일이 다운로드됨.
                final String fileURL = "http://webnautes.tistory.com/attachment/cfile4.uf@267BB53E58451C582BD045.avi";
 
                path= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                outputFile= new File(path, "Alight.avi"); //파일명까지 포함함 경로의 File 객체 생성
 
                if (outputFile.exists()) { //이미 다운로드 되어 있는 경우
 
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setTitle("파일 다운로드");
                    builder.setMessage("이미 SD 카드에 존재합니다. 다시 다운로드 받을까요?");
                    builder.setNegativeButton("아니오",
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                                    int which) {
                                    Toast.makeText(getApplicationContext(),"기존 파일을 플레이합니다.",Toast.LENGTH_LONG).show();
                                    playVideo(outputFile.getPath());
                                }
                            });
                    builder.setPositiveButton("예",
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    outputFile.delete(); //파일 삭제
 
                                    final DownloadFilesTask downloadTask = new DownloadFilesTask(MainActivity.this);
                                    downloadTask.execute(fileURL);
 
                                    progressBar.setOnCancelListener(new DialogInterface.OnCancelListener() {
                                        @Override
                                        public void onCancel(DialogInterface dialog) {
                                            downloadTask.cancel(true);
                                        }
                                    });
                                }
                            });
                    builder.show();
 
                } else { //새로 다운로드 받는 경우
                    final DownloadFilesTask downloadTask = new DownloadFilesTask(MainActivity.this);
                    downloadTask.execute(fileURL);
 
                    progressBar.setOnCancelListener(new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                            downloadTask.cancel(true);
                        }
                    });
                }
            }
        });
    }
 
    private class DownloadFilesTask extends AsyncTask<StringString, Long> {
 
        private Context context;
        private PowerManager.WakeLock mWakeLock;
 
        public DownloadFilesTask(Context context) {
            this.context = context;
        }
 
 
        //파일 다운로드를 시작하기 전에 프로그레스바를 화면에 보여줍니다.
        @Override
        protected void onPreExecute() { //2
            super.onPreExecute();
 
            //사용자가 다운로드 중 파워 버튼을 누르더라도 CPU가 잠들지 않도록 해서
            //다시 파워버튼 누르면 그동안 다운로드가 진행되고 있게 됩니다.
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
            mWakeLock.acquire();
 
            progressBar.show();
        }
 
 
        //파일 다운로드를 진행합니다.
        @Override
        protected Long doInBackground(String... string_url) { //3
            int count;
            long FileSize = -1;
            InputStream input = null;
            OutputStream output = null;
            URLConnection connection = null;
 
            try {
                URL url = new URL(string_url[0]);
                connection = url.openConnection();
                connection.connect();
 
 
                //파일 크기를 가져옴
                FileSize = connection.getContentLength();
 
                //URL 주소로부터 파일다운로드하기 위한 input stream
                input = new BufferedInputStream(url.openStream(), 8192);
 
                path= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                outputFile= new File(path, "Alight.avi"); //파일명까지 포함함 경로의 File 객체 생성
 
                // SD카드에 저장하기 위한 Output stream
                output = new FileOutputStream(outputFile);
 
 
                byte data[] = new byte[1024];
                long downloadedSize = 0;
                while ((count = input.read(data)) != -1) {
                    //사용자가 BACK 버튼 누르면 취소가능
                    if (isCancelled()) {
                        input.close();
                        return Long.valueOf(-1);
                    }
 
                    downloadedSize += count;
 
                    if (FileSize > 0) {
                        float per = ((float)downloadedSize/FileSize) * 100;
                        String str = "Downloaded " + downloadedSize + "KB / " + FileSize + "KB (" + (int)per + "%)";
                        publishProgress("" + (int) ((downloadedSize * 100/ FileSize), str);
 
                    }
 
                    //파일에 데이터를 기록합니다.
                    output.write(data, 0, count);
                }
                // Flush output
                output.flush();
 
                // Close streams
                output.close();
                input.close();
 
 
            } catch (Exception e) {
                Log.e("Error: ", e.getMessage());
            }finally {
                try {
                    if (output != null)
                        output.close();
                    if (input != null)
                        input.close();
                } catch (IOException ignored) {
                }
 
                mWakeLock.release();
 
            }
            return FileSize;
        }
 
 
        //다운로드 중 프로그레스바 업데이트
        @Override
       protected void onProgressUpdate(String... progress) { //4
            super.onProgressUpdate(progress);
 
            // if we get here, length is known, now set indeterminate to false
            progressBar.setIndeterminate(false);
            progressBar.setMax(100);
            progressBar.setProgress(Integer.parseInt(progress[0]));
            progressBar.setMessage(progress[1]);
        }
 
        //파일 다운로드 완료 후
        @Override
        protected void onPostExecute(Long size) { //5
            super.onPostExecute(size);
 
            progressBar.dismiss();
 
            if ( size > 0) {
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다. 파일 크기=" + size.toString(), Toast.LENGTH_LONG).show();
 
                Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                mediaScanIntent.setData(Uri.fromFile(outputFile));
                sendBroadcast(mediaScanIntent);
 
                playVideo(outputFile.getPath());
 
            }
            else
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
        }
 
    }
 
 
    @Override
    public void onRequestPermissionsResult(int permsRequestCode, String[] permissions, int[] grantResults){
        switch(permsRequestCode){
 
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0) {
                    boolean readAccepted = grantResults[0== PackageManager.PERMISSION_GRANTED;
                    boolean writeAccepted = grantResults[1== PackageManager.PERMISSION_GRANTED;
 
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 
                        if ( !readAccepted || !writeAccepted  )
                        {
                            showDialogforPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
                            return;
                        }
                    }
                }
                break;
        }
    }
 
    private void showDialogforPermission(String msg) {
 
        final AlertDialog.Builder myDialog = new AlertDialog.Builder(  MainActivity.this);
        myDialog.setTitle("알림");
        myDialog.setMessage(msg);
        myDialog.setCancelable(false);
        myDialog.setPositiveButton("예"new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface arg0, int arg1) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    requestPermissions(PERMISSIONS, PERMISSION_REQUEST_CODE);
                }
 
            }
        });
        myDialog.setNegativeButton("아니오"new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface arg0, int arg1) {
                finish();
            }
        });
        myDialog.show();
    }
 
    private void playVideo(String path) {
        Uri videoUri = Uri.fromFile(new File(path));
        Intent videoIntent = new Intent(Intent.ACTION_VIEW);
        videoIntent.setDataAndType(videoUri, "video/*");
        if (videoIntent.resolveActivity(getPackageManager()) != null) {
            startActivity(Intent.createChooser(videoIntent, null));
        }
    }
 
 
}
 
 
cs


반응형

해본 것을 문서화하여 기록합니다.
부족함이 있지만 도움이 되었으면 합니다.


포스트 작성시에는 문제 없었지만 이후 문제가 생길 수 있습니다.
질문을 남겨주면 가능한 빨리 답변드립니다.


제가 쓴 책도 한번 검토해보세요 ^^

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기

댓글을 달아 주세요

">
  1. thumbnail
    Favicon of http://ienlab.net BlogIcon 아이엔
    2017.09.02 06:46

    안녕하세요, 혹시 DownloadFIlesTask 부분을 제가 어플리케이션에 적용 가능할련지요?

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.04 15:49 신고

      DownloadFilesTask를 먼저 원하시는 코드로 옮기시고.. 이후 없다고 에러발생하는 부분들을 찾아서 옮기면 될듯합니다..

  2. thumbnail
    Favicon of https://iloveserver.tistory.com BlogIcon RockSlim

    http://webnautes.tistory.com/attachment/cfile4.uf@267BB53E58451C582BD045.avi
    이 부분을 제가 원하는 URL로 바꿀려고 하는데 어떤 방식으로 해야 가능할까요?

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.11 13:50 신고

      티스토리 첨부파일 가져다 사용하려고 하니 주소가 이상해보이는 겁니다.. 서버주소/파일이름 이런식으로 바꾸시면 되요

    • thumbnail
      2017.09.11 23:05

      비밀댓글입니다

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.11 23:32 신고

      기존에 실행중인 asynctask를 강제로 종료시킨후.. 새로운 작업을 시작해야 합니다

    • thumbnail
      Favicon of https://iloveserver.tistory.com BlogIcon RockSlim
      2017.09.11 23:38 신고

      음. onPostExecute가 실행되었는데도 종료가 정상적으로 되지 않은것 인가요?

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.12 04:45 신고

      테스트해봤는데 기존 영상은 항상 지워지는 듯합니다. outputFile.delete();의 리턴값이 항상 True를 리턴합니다.

      혹 기존 영상이 지워지지 않고 플레이된다함은 버튼을 누를때마다 다른 영상이 다운로드되어 플레이되도록 했나요?

      그리고 영상을 2번 이상 받으면 프로그레스바가 아주 잠깐 100부터 시작하는 현상이 있던데 확인해보겠습니다.

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.12 04:46 신고

      onPostExecute까지 처리 완료하면 TASK가 종료된거 맞습니다.

    • thumbnail
      2017.09.12 18:31

      비밀댓글입니다

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.13 11:52 신고

      문제 될건 없어보이는데..음..다음 두 라인 실행전에 기존 파일을 삭제하고 해보세요..

      path= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

      outputFile= new File(path, "Alight.avi"); //파일명까지 포함함 경로의 File 객체 생성

    • thumbnail
      2017.09.13 15:06

      비밀댓글입니다

  3. thumbnail
    2017.09.17 10:51

    비밀댓글입니다

    • thumbnail
      Favicon of https://webnautes.tistory.com BlogIcon webnautes
      2017.09.18 09:18 신고

      네트워크에서 데이터 받아올때에만 필요할거 같아서 버퍼를 사용하도록 했습니다.

  4. thumbnail
    hjc
    2017.10.31 23:58

    작성해 주신 부분을 보면
    final String fileURL = "http://webnautes.tistory.com/attachment/cfile4.uf@267BB53E58451C582BD045.avi"; 부분이 있는데요.
    예를 들어 동영상 주소가 tistory.com/1111.avi라는 동영상 파일을 불러오는 거라고 할때.
    제가 원하는 건 뒤에 1111.avi를 서버측에서 전송해준 파일명으로 원하는파일을 받고싶습니다.
    서버측에 1111 1112 1113 여러개의 동영상파일이존재하면 1111를 서버측에서 받아서 url주소를 바꾸고싶은대요
    바꿀수있는 방법이잇을까요.........ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

    흠..여러 소스를 보면서 공부를 하고 있기는 한데 기초가 없어서 많이 어렵네요.