반응형

AsyncTask를 이용하여 URL 주소로부터 동영상 다운로드하는 작업을 백그라운드 스레드에서 수행하면서 다운로드 진행사항을 프로그레스 다이얼로그에 보여주기 위해서 UI 스레드에 요청하는 과정을 아래 포스팅에서 소개했습니다.

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




AsyncTask 실행 중, 화면 회전시 문제점

위 포스팅에서 소개한대로 하면 문제 없이 동작할 것처럼 보이지만 디바이스의 화면 회전시 IllegalArgumentException라는 예외가 발생합니다.


왜냐하면 디바이스의 화면이 회전할 때, 기존 Activity 인스턴스가 destroy되고 새로운 Activity 인스턴스가 생성되는데 이때 기존 Activity 인스턴스에서 생성되었던 AsyncTask에 대한 처리를 자동으로 해주지 않기 때문입니다.  AsyncTask는 백그라운드 스레드에서 실행되는 doInBackground 메소드의 작업이 완료될 때까지 계속 실행 상태를 유지합니다.  문제는 AsyncTask는 여전히 destroy된 기존 Activity 인스턴스를 참조하고 있기 때문에 발생합니다. 


예를 들어 AsyncTask가 백그라운드 스레드에서 파일 다운로드를 하면서, 진행상태를 프로그레스 다이얼로그에 업데이트하기 위해 UI 스레드에 요청하고 있는 상황이라고 가정해봅시다. 이 때,  디바이스의 화면이 회전하게 되면  새로운  Activity 인스턴스로 교체되지만 계속 실행상태를 유지하고 있는 AsyncTask는 기존 Activity 인스턴스를 참조하여 UI 스레드에 요청을 하게 됩니다 .  하지만 더이상 UI 업데이트를 할 수 없기 때문에 프로그레스 다이얼로그는 사라지게 되고 앱 종료시 예외가 발생하게 됩니다. 


실제 동작으로 확인해보면 다운로드가 시작되고 프로그레스 다이어로그에 계속 다운로드 진행상황을 업데이트 되고 있는 상황에서 


디바이스의 화면을 회전시키게 되면 프로그레스 다이얼로그가 사라지게 되고, 백버튼을 눌러 앱을 종료하면


에러 메시지가 보이게 되고 


안드로이드 스튜디오의 안드로이드 모니터로 확인시 IllegalArgumentException 예외가 발생했음을 확인 할 수 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.tistory.webnautes.asynctask_example, PID: 1325
java.lang.IllegalArgumentException: View=com.android.internal.policy.PhoneWindow$DecorView{2de05ad V.E...... R.....ID 0,0-1554,717} not attached to window manager
   at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:424)
   at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:350)
   at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:118)
   at android.app.Dialog.dismissDialog(Dialog.java:365)
   at android.app.Dialog.dismiss(Dialog.java:348)
   at com.tistory.webnautes.asynctask_example.MainActivity$DownloadFilesTask.onPostExecute(MainActivity.java:253)
   at com.tistory.webnautes.asynctask_example.MainActivity$DownloadFilesTask.onPostExecute(MainActivity.java:138)
   at android.os.AsyncTask.finish(AsyncTask.java:651)
   at android.os.AsyncTask.access$500(AsyncTask.java:180)
   at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:668)
   at android.os.Handler.dispatchMessage(Handler.java:102)
   at android.os.Looper.loop(Looper.java:148)
   at android.app.ActivityThread.main(ActivityThread.java:5527)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)
cs



Fragment를 이용하여 해결

별도의 fragmnet에서 AsyncTask를 생성 및 실행하고  화면이 회전하더라도 이 fragment가 destroy되지 않고 유지되도록 해서 해결합니다. 아래 링크에 있는 설명 및 소스코드를  참고하였습니다.

http://stackoverflow.com/questions/8417885/android-fragments-retaining-an-asynctask-during-screen-rotation-or-configuratio


아래 포스팅에 있는 프로젝트에 적용시켜 봤습니다.

[Android/예제 프로젝트] - Android 예제 - URL 주소로 부터 동영상 다운로드 및 재생( AsyncTask, URLConnection, PowerManager )


fragment에 대한 기초개념은 다음 포스팅에 나와있습니다.

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


실행결과입니다. Portrait에서 다운로드를 진행하다가 Landscape로 바꾸어도 다운로드 진행상태를 표시하는 프로그레스바가 계속 유지됩니다.





MainActivity

MainActivity.java

MainActivty에서는 따로 하는 일은 없고 MainFragment를 추가하여 유저인터페이스를 구성합니다.  앱을 위한 실제 작업들은 MainFragment에서 모두 이루어지지만 Activity없이 Fragment 독자적으로 동작할 수 없기 때문에 MainActivity가 필요합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tistory.webnautes.asynctask_example;
 
import android.app.Activity;
import android.os.Bundle;
 
public class MainActivity extends Activity{
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
cs


activity_main.xml

fragment태그를 사용하여 MainFragment를 레이아웃에 추가합니다. 이때 class 속성에 MainFragment를 지정해주어야 합니다.

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"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent" >
 
    <fragment
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.tistory.webnautes.asynctask_example.MainFragment"
        tools:layout="@layout/fragment_main" />
 
</LinearLayout>
cs



MainFragment & TaskFragment


MainFragment는 MainActivty가 재생성시 같이 재생성됩니다. 그때마다 TaskFragment를 찾아서 자신에게 처리 결과를 넘겨주도록 지정해줍니다. 실제 다운로드 작업은 AsyncTask를 상속받은 Task 클래스에서 이루어지며 TaskFragment에서 관리하게 됩니다. 그러기 위해서  MainAcitivity와 MainFragment가 재생성되더라도 TaskFragment는 Destroy되지 않고 계속 유지되게 만들어 줍니다.(TaskFragment의 onCreate메소드에서 setRetainInstance(true)를 호출)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        //화면 전환으로 MainFragment가 재생성되었을 경우를 고려해준다.
        //아직 동작 중일 TaskFragment를 태그로 찾아서
         TaskFragment taskFragment = (TaskFragment) getFragmentManager().findFragmentByTag(TASK_FRAGMENT_TAG);
 
        if (taskFragment != null) {//만약 TaskFragment가 동작 중이라면
 
            //setTargetFragment로 Target Fragment를 새로 생성된 MainFragment 인스턴스로 교체한다.
            //TaskFragment에서 getTargetFragment().onActivityResult를 호출하면 MainFragment의 onActivityResult가 호출되도록 설정된다.
            taskFragment.setTargetFragment(this, DIALOG_REQUEST_CODE);
        }
    }
cs


TaskFragment에서 넘겨준 다운로드 결과를 받아서 완료시에는 동영상을 플레이할 준비를 해주고, 실패했을 경우에는 실패 메시지만 보여줍니다.

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
    //taskFragment에서 getTargetFragment().onActivityResult를 호출하면 이 메소드가 호출된다.
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == DIALOG_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
 
            Integer downloadResult = data.getExtras().getInt("downloadResult");
            String message = null;
 
            if ( downloadResult == 0 ) {
                message = "다운로드 완료되었습니다.";
                Log.d( TAG, message );
                Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
 
                File externalStorageFile = new File(externalStorageFileFullPath);
 
                Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                mediaScanIntent.setData(Uri.fromFile(externalStorageFile));
                getActivity().sendBroadcast(mediaScanIntent);
 
                playVideo(externalStorageFile.getPath());
 
            }
            else {
                message = "다운로드가 중단되었습니다.";
 
                Log.d( TAG, message );
                Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
            }
        }
    }
cs


TaskFragment에서는 Task가 요청한 프로그레스바 상태 업데이트를 대신 처리해줍니다. 이렇게 함으로써 화면이 회전하더라도 프로그레스바가 사라지지 않게됩니다. 또한 Task처리 결과를 MainFragment로 넘겨주는 역활도 하게 됩니다. 

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
        //다운로드 진행상태를 프로그레스바에 표시하기 위해 AsyncTask에서 호출한다.
        public void updateProgress(String... progressState) {
            mProgressBar.setProgress(Integer.parseInt(progressState[0]));
            mProgressBarText.setText(progressState[1]);
            Log.d( TAG, "TaskFragment:updateProgress "+ progressState[1]);
        }
 
        //다운로드가 완료된 경우 AsyncTask에서 호출한다.
        public void taskFinished(Integer downloadResult) {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();
 
            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;
 
            //결과값을 MainFragment에게 전달한다.
            if (getTargetFragment() != null) {
                Intent data = new Intent();
                data.putExtra ("downloadResult", downloadResult);
                getTargetFragment().onActivityResult( getTargetRequestCode(), Activity.RESULT_OK, data);
            }
        }
cs


MainFragment.java 전체 코드입니다.  TaskFragment를 별도의 파일로 나누어도 되지만 포함시켜 두었습니다.

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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
package com.tistory.webnautes.asynctask_example;
 
 
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
 
import java.io.File;
 
 
public class MainFragment extends Fragment implements OnClickListener {
 
    TextView orientationTextView;
    private static final int DIALOG_REQUEST_CODE = 1234;
    String internetFileURL;
    String externalStorageFileFullPath;
    final String TAG = "MainFragment";
    static final int PERMISSION_REQUEST_CODE = 1;
    String[] PERMISSIONS = {"android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE"};
 
    //화면 회전으로 MainActivty와 MainFragment가 재생성되고 나서 MainFragment에서 TaskFragment를 다시 찾기 위해서 태그를 사용한다.
    static final String TASK_FRAGMENT_TAG = "task";
 
    //화면 전환하면 방향에 맞추어 TextView에 문자열을 출력한다.
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
 
        switch(newConfig.orientation){
            case Configuration.ORIENTATION_LANDSCAPE:
                orientationTextView.setText("Landscape");
                break;
 
            case Configuration.ORIENTATION_PORTRAIT:
                orientationTextView.setText("Portrait");
                break;
        }
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
        return inflater.inflate(R.layout.fragment_main, container, false);
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        //화면 전환으로 MainFragment가 재생성되었을 경우를 고려해준다.
        //아직 동작 중일 TaskFragment를 태그로 찾아서
         TaskFragment taskFragment = (TaskFragment) getFragmentManager().findFragmentByTag(TASK_FRAGMENT_TAG);
 
        if (taskFragment != null) {//만약 TaskFragment가 동작 중이라면
 
            //setTargetFragment로 Target Fragment를 새로 생성된 MainFragment 인스턴스로 교체한다.
            //TaskFragment에서 getTargetFragment().onActivityResult를 호출하면 MainFragment의 onActivityResult가 호출되도록 설정된다.
            taskFragment.setTargetFragment(this, DIALOG_REQUEST_CODE);
        }
    }
 
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
 
        if (!hasPermissions(PERMISSIONS)) { //퍼미션 허가를 했었는지 여부를 확인
            requestNecessaryPermissions(PERMISSIONS);//퍼미션 허가안되어 있다면 사용자에게 요청
        }
 
        //버튼 클릭시 이벤트를 MainFragment에서 구현한다.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
 
        orientationTextView = (TextView) view.findViewById(R.id.OrientationTextView);
 
        //웹브라우저에 아래 링크를 입력하면 Alight.avi 파일이 다운로드됨.
        internetFileURL = "http://webnautes.tistory.com/attachment/cfile4.uf@267BB53E58451C582BD045.avi";
        File externalStorageDirectoryPath= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        externalStorageFileFullPath = externalStorageDirectoryPath.getPath() + "Alight.avi";
    }
 
 
    @Override
    public void onClick(View v) {
 
        final File externalStorageFile = new File(externalStorageFileFullPath);
 
        if (externalStorageFile.exists()) { //이미 다운로드 되어 있는 경우
 
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle("파일 다운로드");
            builder.setMessage("이미 SD 카드에 존재합니다. 다시 다운로드 받을까요?");
            builder.setNegativeButton("아니오",
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog,
                                            int which) {
                            Toast.makeText( getActivity(),"기존 파일을 플레이합니다.",Toast.LENGTH_LONG).show();
                            playVideo( externalStorageFileFullPath);
                        }
                    });
            builder.setPositiveButton("예",
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {//다시 다운로드 받는 경우
                            //기존 파일 삭제
                            externalStorageFile.delete();
 
                            //새로운 TaskFragment 생성
                            TaskFragment taskFragment = new TaskFragment();
 
                            //Task를 생성하여 taskFragment의 setTask메소드에서 Task 인스턴스를 저장하고, Task에게 taskFragment 인스턴스를 넘겨준다.
                            //TaskFragment의 onCreate메소드에서 Task를 실행하게 된다.
                            taskFragment.setTask(new Task(externalStorageFileFullPath, internetFileURL));
 
                            //taskFragment에서 getTargetFragment().onActivityResult를 호출하면 MainFragment의 onActivityResult가 호출되도록 설정
                            taskFragment.setTargetFragment( MainFragment.this, DIALOG_REQUEST_CODE);
 
                            //프레그먼트를 보여준다. 태그는 나중에 프레그먼트를 찾기 위해 사용 된다.
                            taskFragment.show(getFragmentManager(), TASK_FRAGMENT_TAG);
                        }
                    });
            builder.show();
 
        } else { //새로 다운로드 받는 경우
            //새로운 TaskFragment 생성
            TaskFragment taskFragment = new TaskFragment();
 
            //Task를 생성하여 taskFragment의 setTask메소드에서 Task 인스턴스를 저장하고, Task에게 taskFragment 인스턴스를 넘겨준다.
            taskFragment.setTask(new Task(externalStorageFileFullPath, internetFileURL));
 
            //taskFragment에서 getTargetFragment().onActivityResult를 호출하면 MainFragment의 onActivityResult가 호출되도록 설정
            taskFragment.setTargetFragment( MainFragment.this, DIALOG_REQUEST_CODE );
 
            //프레그먼트를 보여준다.태그는 나중에 프레그먼트를 찾기 위해 사용 된다.
            taskFragment.show(getFragmentManager(), TASK_FRAGMENT_TAG);
         }
    }
 
    //taskFragment에서 getTargetFragment().onActivityResult를 호출하면 이 메소드가 호출된다.
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == DIALOG_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
 
            Integer downloadResult = data.getExtras().getInt("downloadResult");
            String message = null;
 
            if ( downloadResult == 0 ) {
                message = "다운로드 완료되었습니다.";
                Log.d( TAG, message );
                Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
 
                File externalStorageFile = new File(externalStorageFileFullPath);
 
                Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                mediaScanIntent.setData(Uri.fromFile(externalStorageFile));
                getActivity().sendBroadcast(mediaScanIntent);
 
                playVideo(externalStorageFile.getPath());
 
            }
            else {
                message = "다운로드가 중단되었습니다.";
 
                Log.d( TAG, message );
                Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
            }
        }
    }
 
    //플레이어를 선택하여 fullPath 위치에 다운로드 받은 동영상을 볼수 있도록 해준다.
    private void playVideo(String fullPath) {
        Uri videoUri = Uri.fromFile(new File(fullPath));
        Intent videoIntent = new Intent(Intent.ACTION_VIEW);
        videoIntent.setDataAndType(videoUri, "video/*");
        if (videoIntent.resolveActivity(getActivity().getPackageManager()) != null) {
            startActivity(Intent.createChooser(videoIntent, null));
        }
    }
 
    private boolean hasPermissions(String[] permissions) {
        int res = 0;
        //스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인
        for (String perms : permissions){
            res = getActivity().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
    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(getActivity());
        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) {
                getActivity().finish();
            }
        });
        myDialog.show();
    }
 
 
    //MainFragment는 화면 회전시 destroy되기 때문에
    //DialogFragment를 상속받은 TaskFragment에서 AsyncTask를 저장하게 된다.
    //onCreate에서 setRetainInstance(true)를 해주기 때문에 이 프래그먼트는 화면 회전해도 destroy되지 않는다.
    //그 결과 Task가 보존된다.
    public static class TaskFragment extends DialogFragment {
 
        Task mTask;
        ProgressBar mProgressBar;
        TextView mProgressBarText;
        final String TAG = "TaskFragment";
 
        public void setTask(Task task) {
            mTask = task;
 
            //updateProgress()와 taskFinished() 처리가 이 프레그먼트에서 이루어짐을 AsyncTask에게 알려준다.
            mTask.setFragment(this);
        }
 
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
 
            //화면 회전으로 MainActivity와 MainFragment가 destroy되더라도 TaskFragment 인스턴스 유지시켜줌
            setRetainInstance(true);
 
            //Task를 시작한다.
            if (mTask != null)
                mTask.execute();
        }
 
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_dialog, container);
            mProgressBar = (ProgressBar) view.findViewById(R.id.progressBar);
            mProgressBarText = (TextView) view.findViewById(R.id.progressBarText);
 
            getDialog().setTitle("Progress Dialog");
 
            //다이얼로그 밖을 터치하더라도 다이얼로그를 닫지 못하도록 설정
            getDialog().setCanceledOnTouchOutside(true);
 
            return view;
        }
 
        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView() {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }
 
        //뒤로가기를 눌러서 다운로드를 취소한 경우, 다이얼로그가 사라진다.
        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
 
            //Task가 동작중이라면 취소(cancel) 시킨다.
            if (mTask != null)
                mTask.cancel(false);
 
            //결과값을 MainFragment에게 전달한다.
            if (getTargetFragment() != null) {
                Intent data = new Intent();
                int downloadResult = -1;
                data.putExtra("downloadResult", downloadResult);
                getTargetFragment().onActivityResult( getTargetRequestCode(), Activity.RESULT_CANCELED, data );
            }
        }
 
        @Override
        public void onResume() {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }
 
        //다운로드 진행상태를 프로그레스바에 표시하기 위해 AsyncTask에서 호출한다.
        public void updateProgress(String... progressState) {
            mProgressBar.setProgress(Integer.parseInt(progressState[0]));
            mProgressBarText.setText(progressState[1]);
            Log.d( TAG, "TaskFragment:updateProgress "+ progressState[1]);
        }
 
        //다운로드가 완료된 경우 AsyncTask에서 호출한다.
        public void taskFinished(Integer downloadResult) {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();
 
            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;
 
            //결과값을 MainFragment에게 전달한다.
            if (getTargetFragment() != null) {
                Intent data = new Intent();
                data.putExtra ("downloadResult", downloadResult);
                getTargetFragment().onActivityResult( getTargetRequestCode(), Activity.RESULT_OK, data);
            }
        }
    }
 
}
cs


fragment_main.xml

MainFragment에서 사용하는 레이아웃으로 화면 회전방향을 표시하기 위한 TextView와 다운로드 시작을 위한 버튼으로 구성되어 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="top"
              android:orientation="vertical" >
 
    <TextView
        android:id="@+id/OrientationTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Portrait"
        android:textAppearance="?android:attr/textAppearanceLarge" />
 
    <Button
        android:id="@+id/taskButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Task" />
 
</LinearLayout>
cs


fragment_dialog.xml

TaskFragment에서 사용하는 레이아웃으로 다운로드 진행상태를 택스트와 프로그래스바 두가지로 보여주게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:orientation="vertical" >
 
    <TextView
        android:id="@+id/progressBarText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceMedium" />
 
    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0" />
 
</LinearLayout>
cs



Task

task.java

Task는 AsyncTask를 상속받은 클래스로 실제 동영상 파일 다운로드를 백그라운드에서 진행하게 됩니다.  백그라운드 작업중 프로그레스바 상태 업데이트 관련 UI 처리를 자신이 요청 하지 않고 TaskFragment에서 하도록 하고, 또한 종료 전 다운로드 상태를 TaskFragment에게 넘겨줍니다.  이렇게 해줌으로써 화면이 회전하더라도 프로그레스바가 사라지지 않고 계속 유지됩니다.

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
package com.tistory.webnautes.asynctask_example;
 
import android.content.Context;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.util.Log;
 
 
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 Task extends AsyncTask<Void, String, Integer> {
 
    private PowerManager.WakeLock mWakeLock;
    MainFragment.TaskFragment mFragment;
 
    String mInternetFileURL;
    String mExternalStorageFileFullPath;
 
 
    public Task(String externalStorageFileFullPath, String internetFileURL) {
        mExternalStorageFileFullPath = externalStorageFileFullPath;
        mInternetFileURL = internetFileURL;
    }
 
    void setFragment(MainFragment.TaskFragment fragment) {
        mFragment = fragment;
    }
 
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
 
        //사용자가 다운로드 중 파워 버튼을 누르더라도 CPU가 잠들지 않도록 해서
        //다시 파워버튼 누르면 그동안 다운로드가 진행되고 있게 됩니다.
        PowerManager powerManager = (PowerManager) mFragment.getActivity().getSystemService(Context.POWER_SERVICE);
        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
        mWakeLock.acquire();
    }
 
 
    //파일 다운로드를 진행합니다.
    @Override
    protected Integer doInBackground(Void... unused) {
        int count;
        long FileSize = 0;
        InputStream input = null;
        OutputStream output = null;
        URLConnection connection = null;
        long downloadedSize = 0;
 
        try {
            URL url = new URL(mInternetFileURL);
            connection = url.openConnection();
            connection.connect();
 
 
            //파일 크기를 가져옴
            FileSize = connection.getContentLength();
 
            //URL 주소로부터 파일다운로드하기 위한 input stream
            input = new BufferedInputStream(url.openStream(), 8192);
 
            // SD카드에 저장하기 위한 Output stream
            File outputFile= new File(mExternalStorageFileFullPath);
            output = new FileOutputStream(outputFile);
 
 
            byte data[] = new byte[1024];
            while ((count = input.read(data)) != -1) {
 
                //사용자가 BACK 버튼 누르면 취소가능
                if (isCancelled()) {
                    input.close();
 
                    return -1;
                }
 
                downloadedSize += count;
 
                if (FileSize > 0) {
                    int percent = (int)(((float)downloadedSize/FileSize) * 100);
                    String progressText = "Downloaded " + downloadedSize + "KB / " + FileSize + "KB (" + (int)percent + "%)";
                    publishProgress(String.valueOf(percent), progressText);
                }
 
                //파일에 데이터를 기록합니다.
                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 0;
    }
 
    //다운로드 중 프로그레스바 업데이트
    @Override
    protected void onProgressUpdate(String... progressState) {
        if (mFragment == null)
            return;
        mFragment.updateProgress(progressState);
    }
 
    //다운로드 중단
    @Override
    protected void onCancelled(Integer result) {
        if (mFragment == null)
            return;
        mFragment.taskFinished(result);
    }
 
    //다운로드 완료
    @Override
    protected void onPostExecute(Integer result) {
        if (mFragment == null)
            return;
 
        mFragment.taskFinished(result);
    }
 
}
cs


반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


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

+ Recent posts