이제 안드로이드의 DownloadProvider 소스를 읽어보도록 하겠습니다.

14212 단어 Android
이 글은 CrowdWorks 추가 캘린더의 다섯째 날의 글이다.
#CrowdWorks의 엔지니어 매일 무엇을 쓰고 있습니까?

미리 준비하다


저는 클라우드 컴퓨팅 회사에서 안드로이드 응용 프로그램과 API 개발@YusukeIwaki에 종사합니다.
이전 작업으로 안드로이드 OS 맞춤형 작업을 많이 했기 때문에 지금도 안드로이드의 출처를 바라보며'이 디자인이 좋구나'생각하며 그걸 모방해 애플리케이션을 만들고 있다.
이번에 제가 쓰고자 하는 것은 안드로이드 파일 다운로드 기능의 기초, DownloadProvider의 뛰어난 디자인을 책임지는 것입니다.
나는 DownloadProvider 자체가 5년 전부터 있었다고 생각한다. 이제야 이런 느낌을 갖게 되었다. 그러나 클라우드에서 일하는 공식 안드로이드 앱은 DownloadProvider의 디자인에 큰 참고 가치가 있기 때문에 CrowdWorks를 부가 달력 재료로 한다.
 

DownloadProider 주제에 들어가기 전에


이거 어떻게 이루어져요?



  실복에 AsyncTask라는 단어가 등장했다면 끝까지 읽어주세요.
  • API 통신 중 화면이 회전해도 API를 이중으로 호출하지 않습니까?
  • API 통신에서 이전 화면으로 돌아가도 정상적으로 작동합니까?
  • API 커뮤니케이션에서 어플리케이션을 죽이면 어떻게 됩니까?
  • 라는 AsyncTask는 아픈 점을 사용하고, DownloadProvider는 멋진 디자인으로 화려하게 제거하고 있다.
     
     

    DownloadProvider의 전체상


    굉장히 대략적인 그림으로 표현하면...

    DownloadManager = 다운로드 작업을 요청하는 애플리케이션을 위한 API 레이어
    DownloadProvider = 다운로드 대기열과 진행 상황을 관리하는 데이터 저장소
    DownloadService = 데이터 저장소의 변경 또는 통신 상태를 모니터링(3G/Wifi/범위 외) 필요에 따라 파일 다운로드를 시작/중단/복원하는 서비스
    DownloadThread = HTTP 클라이언트를 통해 파일을 다운로드합니다.데이터 저장소에 다운로드 진행 상태 업데이트
    DocumentsUi,DownloadStorageProvider = 데이터가 저장된 내용(대기열과 진행 상황 등)을 시야에 직접 비추다
    이러한 역할 분담으로 안드로이드의 파일 다운로드 처리가 완료됐다.
     
    중요한 것은 보기가 통신 측의 상태를 전혀 고려하지 않고 통신할 권한도 없다는 것이다.다만 데이터 상점에 쓴 내용을 좀 더 적극적으로 반영할 뿐이다.
    따라서 화면이 회전할 때 갑자기 살해되는 작업은 파일 다운로드 처리에 영향을 미치지 않는다.
    멋진 디자인'의 개요다.
     
    여기서부터 각 구성 요소를 다시 상세하게 보아라."이미 인기 있는 디자인을 알았으니..."이런 분들은 건너뛰고 한숨에 끝까지 가세요.

    DownloadManager


    이것은 응용 프로그램에서 "이 파일을 다운로드하라"는 API 레이어입니다.request()이 함수는 내용으로 볼 때 단순히Content Resolver를 통해DownloadProvider의 데이터베이스 insert에 요청했을 뿐이다.
        /**
         * Enqueue a new download.  The download will start automatically once the download manager is
         * ready to execute it and connectivity is available.
         *
         * @param request the parameters specifying this download
         * @return an ID for the download, unique across the system.  This ID is used to make future
         * calls related to this download.
         */
        public long enqueue(Request request) {
            ContentValues values = request.toContentValues(mPackageName);
            Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
            long id = Long.parseLong(downloadUri.getLastPathSegment());
            return id;
        }
    
    Request 의 내용은?이렇게 말하면
        /**
         * This class contains all the information necessary to request a new download. The URI is the
         * only required parameter.
         *
         * Note that the default download destination is a shared volume where the system might delete
         * your file if it needs to reclaim space for system use. If this is a problem, use a location
         * on external storage (see {@link #setDestinationUri(Uri)}.
         */
        public static class Request {
    
            (中略)
    
            private Uri mUri;
            private Uri mDestinationUri;
            private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
            private CharSequence mTitle;
            private CharSequence mDescription;
            private String mMimeType;
            private int mAllowedNetworkTypes = ~0; // default to all network types allowed
            private boolean mRoamingAllowed = true;
            private boolean mMeteredAllowed = true;
            private boolean mIsVisibleInDownloadsUi = true;
            private boolean mScannable = false;
            private boolean mUseSystemCache = false;
    
  • 파일 다운로드 URL
  • 다운로드 중 알림 영역에 표시하고 싶은 제목 등
  • 모바일 네트워크에서도 다운로드 여부(Wifi 접속이 없는 경우)
  • HTTP 통신에 사용되는 사용자 정의 헤드(User-Agent 또는 인증 1등)
  • 잠깐만요.
    $ adb shell
     @android:/# su 1000
     @android:/$ content query --uri content://downloads/all_downloads
    
    시뮬레이터라면 ↑ 명령을 입력하면 실제 데이터베이스에 기록된 Request 내용을 볼 수 있습니다.
     

    DownloadProvider



    Content Resolver insert 에서 얻은 정보를 데이터베이스로 직접 가져오는 김에
  • DB 변경 통지 보내기
  • DownloadService(keep-alive)
  • 차기
    이런 일을 하면서.

    변경 알림을 보내는 것은 보기 측면에서 데이터를 다시 읽기 위해서입니다.
    Download Service를 날리는 것은 이미 죽었을 수도 있는 Download Service가 확실히 존재하는 상태가 되도록 insert의 다운로드 요청을 정확하게 처리하기 위해서입니다.
    여기에는 쓰지 않습니다. 업데이트와 delete도 비슷한 논리가 있습니다.
    안드로이드의 서비스는 배후에서 계속 생존할 수 있다는 보장이 전혀 없다. 매번 insert/update/delete일 때keep-alive는 매우 합리적인 디자인이다.
     

    DownloadService


    DownloadProvider 데이터베이스에 적힌 내용과 통신 상태를 감시하고 다운로드할 내용이 있으면 DownloadTheread를 준비하여 다운로드를 시작하거나 다시 시작하거나 다운로드를 중지합니다.

    앞서 말한 바와 같이, Download 서비스는 그가 언제 죽고, 언제 부활하길 바라는 보증이 전혀 없다.
  • 내부 상태 모두 DB에 기록
  • DB에 기록된 내용에 따라 고유하게 이동
  • 이런 구조.
    예를 들어 DownloadThread를 준비하여 다운로드를 시작한 Request는 상태를 RUNNING으로 저장하여 이중 요청을 일으키지 않습니다.

    DownloadThread


    Download Service의 막내 동생으로서 HTTP 클라이언트를 통해 실제로 통신한다.
    이 DownloadTheread도 이런 동작을 했다. Download 서비스가 갑자기 죽거나 부활해도 곤란하지 않도록 8KB를 다운로드한 후 DB에 진도를 업데이트하고...8KB를 다운로드한 후 DB에 진도를 업데이트한다.
        /**
         * Transfer as much data as possible from the HTTP response to the
         * destination file.
         */
        private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
                throws StopRequestException {
            final byte buffer[] = new byte[Constants.BUFFER_SIZE];
            while (true) {
                checkPausedOrCanceled();
    
                int len = -1;
                try {
                    len = in.read(buffer);
                } catch (IOException e) {
                    throw new StopRequestException(
                            STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
                }
    
                if (len == -1) {
                    break;
                }
    
                try {
                    // When streaming, ensure space before each write
                    if (mInfoDelta.mTotalBytes == -1) {
                        final long curSize = Os.fstat(outFd).st_size;
                        final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
    
                        StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
                    }
    
                    out.write(buffer, 0, len);
    
                    mMadeProgress = true;
                    mInfoDelta.mCurrentBytes += len;
    
                    updateProgress(outFd);
    
                } catch (ErrnoException e) {
                    throw new StopRequestException(STATUS_FILE_ERROR, e);
                } catch (IOException e) {
                    throw new StopRequestException(STATUS_FILE_ERROR, e);
                }
            }
    
    ------------
    
        /**
         * Local changes to {@link DownloadInfo}. These are kept local to avoid
         * racing with the thread that updates based on change notifications.
         */
        private class DownloadInfoDelta {
    
            private ContentValues buildContentValues() {
                final ContentValues values = new ContentValues();
    
                values.put(Downloads.Impl.COLUMN_URI, mUri);
                values.put(Downloads.Impl._DATA, mFileName);
                values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
                values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
                values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
                values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
                values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
                values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
                values.put(Constants.ETAG, mETag);
    
                values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
                values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
    
                return values;
            }
    
            /**
             * Blindly push update of current delta values to provider.
             */
            public void writeToDatabase() {
                mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(),
                        null, null);
            }
    
    언뜻 보기에는 비효율적인 일을 한 것 같지만 이렇게 되면 다운로드 중 갑자기 범위 밖으로 나갔다가 다시 다운로드를 시작하는 경우라도 바이트부터 다시 다운로드할 수 있다!Etag 헤드가 부착된 요구를 할 수 있기 때문에 통신비를 낭비하지 않으면 된다.
     

    DocumentsUi、DownloadStorageProvider


    뷰 측면이지만 처음 그린 것처럼 DownloadProvider의 DB를 유일하게 비추는 것일 뿐입니다.

    예를 들어 상태 알림 패널의 다운로드 진도를 클릭할 때 나타나는 안드로이드 표준 구성 요소는
        public void bindView(View convertView, int position) {
            if (!(convertView instanceof DownloadItem)) {
                return;
            }
    
            long downloadId = mCursor.getLong(mIdColumnId);
            ((DownloadItem) convertView).setData(downloadId, position,
                    mCursor.getString(mFileNameColumnId),
                    mCursor.getString(mMediaTypeColumnId));
    
            // Retrieve the icon for this download
            retrieveAndSetIcon(convertView);
    
            String title = mCursor.getString(mTitleColumnId);
            if (title.isEmpty()) {
                title = mResources.getString(R.string.missing_title);
            }
            setTextForView(convertView, R.id.download_title, title);
            setTextForView(convertView, R.id.domain, mCursor.getString(mDescriptionColumnId));
            setTextForView(convertView, R.id.size_text, getSizeText());
    
            final int status = mCursor.getInt(mStatusColumnId);
            final CharSequence statusText;
            if (status == DownloadManager.STATUS_SUCCESSFUL) {
                statusText = getDateString();
            } else {
                statusText = mResources.getString(getStatusStringId(status));
            }
            setTextForView(convertView, R.id.status_text, statusText);
    
            ((DownloadItem) convertView).getCheckBox()
                    .setChecked(mDownloadList.isDownloadSelected(downloadId));
        }
    
    (※ 설명을 위해 안드로이드 4.2 시대의 소스만 실렸습니다. 요즘은 복잡합니다...^^;)
    이런 느낌이에요.
    다운로드를 요청하는 응용 프로그램에 있어서
  • 파일을 다운로드할 때 호두를 표시하고 싶다
  • 다운로드 완료 후 확인 표시
  • 이러한 UI를 원한다면 DownloadProvider를 감시하는 DB를 추가하여 보기에 반영하는 논리만 추가하면 됩니다.

    잔소리가 많은 것 같지만 어디서 어떤 UI를 보내든 통신 상태는 전혀 신경 쓰지 않고 DB의 내용만 반영하면 돼!
     
     
     

    최후


    나는 반드시 처음에 쓴 것을 다시 한 번 쓸 것이다.

    이거 어떻게 이루어져요?



    나는 더 이상 AsyncTask를 사용하고 싶지 않다!
     
  • DB 제작 메시지 테이블 준비
  • 메시지의 도례는 Message 표를 유일하게 비추는 것
  • 발송 버튼을 눌렀을 때 insert "body=안돼,status=API 통신 대기"의 기록
  • 배후에서 이동하는 서비스를 제작하여 순서대로 API 통신을 진행한다. 성공한 후'status=성공', 실패한 후'status=실패'
  • 로 업데이트한다.
    이런 거 하고 싶어요?
     
    클라우드 작업의 공식 안드로이드 앱은 실제로 이 구조를 채택했다

    '실패한 메시지를 다시 보낼 수 있다'는 상태 변화가 번거롭다는 논리도 있다
  • "status=발송 실패"의 기록을 클릭하면 재발급 확인 대화상자
  • 가 나타납니다
  • 재발신 확인 대화상자에서'재발신'을 선택하면 이 기록을'status=API 통신 대기'
  • 로 업데이트합니다
    이렇게 극히 간단한 논리가 실현되었다.
    #구체적 실행방안을 염두에 두고 계신 분들은 Realm 자동 캘린더작성할 때 하는 것샘플 프로그램을 꼭 보십시오.

    그러므로


    이 글은 CrowdWorks Advent Calendar 2016의 5일째 기사입니다.
    내일@takeru0757선생님은 뭘 좀 쓰세요.

    좋은 웹페이지 즐겨찾기