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되지 않고 유지되도록 해서 해결합니다. 아래 링크에 있는 설명 및 소스코드를 참고하였습니다.
아래 포스팅에 있는 프로젝트에 적용시켜 봤습니다.
[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 |
'Android > 개념 및 예제' 카테고리의 다른 글
Android - ArrayList의 String을 TextView에 출력하는 예제( strings.xml의 문자열 서식 사용) (0) | 2017.08.11 |
---|---|
java 예제 - 16진수 문자열(hex string)와 바이트 배열(byte array)간 변환하는 방법 (0) | 2017.04.24 |
안드로이드 개념 및 예제 - AlertDialog (15) | 2016.12.28 |
안드로이드 개념 및 예제 - Fragment (9) | 2016.12.19 |
안드로이드 개념 및 예제 - AsyncTask (14) | 2016.12.07 |
시간날때마다 틈틈이 이것저것 해보며 블로그에 글을 남깁니다.
블로그의 문서는 종종 최신 버전으로 업데이트됩니다.
여유 시간이 날때 진행하는 거라 언제 진행될지는 알 수 없습니다.
영화,책, 생각등을 올리는 블로그도 운영하고 있습니다.
https://freewriting2024.tistory.com
제가 쓴 책도 한번 검토해보세요 ^^
그렇게 천천히 걸으면서도 그렇게 빨리 앞으로 나갈 수 있다는 건.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!