Android

[Android]Retrofit을 이용한 apk 파일 다운로드 및 설치

park_juyoung 2019. 6. 26. 11:48

개발 도중 플레이스토어에 올라가있지 않은 앱을 업데이트 하기위하여 앱에서 노티를 띄워 apk파일을 받아 설치하는 것을 구현하였습니다.

 

백그라운드에서 다운로드를 하고 notification에 progress를 업데이트 하기위해 아래와 같은 코드를 작성해야합니다.

1. Project Setup

build.gradle에 retrofit의존성을 추가해줍니다.

1
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
 

Manifest에 아래와 같은 권한을 설정합니다.

1
2
3
4
5
6
7
<uses-permission android:name="android.permission.INTERNET"/>
<!--외부 저장소에 파일 저장하기 위한 권한-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--oreo 이상에서 foreground 서비스를 위한 권한-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--다운로드 받은 앱을 설치 하기위한 권한-->
<uses-permission android:name = "android.permission.REQUEST_INSTALL_PACKAGES" />
 

또한 Android 7.0(Nougat / API 24)부터 Intent로 URI 파일 경로 전송시 "file://" 노출되어 있으면 FileUriExposedException 오류가 발생하게 되고 앱이 종료됩니다. 따라서 앱간 파일을 공유하려면 "file://" 대신 "content://"로 URI를 보내야 합니다.

URI로 데이터를 보내기 위해선 액세스 권한을 부여해야 하고 FileProvider를 이용해야 합니다.

application 안에 provider와 service를 정의해줍니다.

1
2
3
4
5
6
7
8
9
10
11
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>
       
<service android:name=".service.DownloadNotificationService"/>
 

액세스 권한을 위하여 res/xml/file_paths.xml 생성 후 아래와 같이 작성해줍니다.

아래와 같이 작성시 외부저장소에 모든하위 경로에 접근할 수 있게됩니다.

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
    <external-path name="external_files" path="."/>
</paths>
 
 
 

2. 서버로 apk파일을 요청하기 위한 Retrofit 정의

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
public class ApiService {
    public Api api;
    private String API_URL = "Your_URL";
 
    private ApiService() {
    
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(API_URL)
                .build();
 
        api = retrofit.create(Api.class);
 
    }
 
    public static ApiService getInstance() {
        return Holder.apiService;
    }
 
    private static class Holder {
        public static final ApiService apiService = new ApiService();
 
    }
 
    interface Api {
 
        @GET
        @Streaming
        Call<ResponseBody> downloadFile(@Url String url);
    }
 
}
 
 

3. apk파일 다운로드 작업을 위한 Service 정의

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
 
public class DownloadNotificationService extends IntentService {
    public static final String PROGRESS_UPDATE = "progress_update";
    private NotificationCompat.Builder notificationBuilder;
    private NotificationManager notificationManager;
    public DownloadNotificationService() {
        super("downloadService");
    }
 
 
    //onHandleIntent는 워커스레드
    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 
        // oreo이상은 notificationChannel을 생성해주어야함.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 
            NotificationChannel notificationChannel = new NotificationChannel("download""파일 다운로드", NotificationManager.IMPORTANCE_HIGH);
            notificationChannel.setDescription("description"); //setting 에서 앱의 알림의 대한 설명
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.enableVibration(false);
            notificationManager.createNotificationChannel(notificationChannel);
        }
 
        notificationBuilder = new NotificationCompat.Builder(this"download")
                .setSmallIcon(android.R.drawable.stat_sys_download)
                .setContentTitle("다운로드입니다")
                .setContentText("다운로드중")
                .setDefaults(0)
                .setAutoCancel(true);
 
        notificationManager.notify(0notificationBuilder.build());
 
        Call<ResponseBody> request = ApiService.getInstance().api.downloadFile("your file path");
 
        try {
            downloadFile(request.execute().body());
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }
 
    private void downloadFile(ResponseBody body) throws IOException {
 
        int count;
        byte data[] = new byte[1024 * 4];
        long fileSize = body.contentLength();
        InputStream inputStream = new BufferedInputStream(body.byteStream(), 1024 * 8);
        File outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "your.apk");
 
        // 파일이 존재한다면 지우고 다시 받기 위하여
        if (outputFile.exists()) {
            outputFile.delete();
        }
 
        OutputStream outputStream = new FileOutputStream(outputFile);
 
        long total = 0;
        boolean downloadComplete = false;
 
        while ((count = inputStream.read(data)) != -1) {
 
            total += count;
            int progress = (int) ((double) (total * 100/ (double) fileSize);
 
 
            updateNotification(progress);
            outputStream.write(data, 0, count);
            downloadComplete = true;
        }
 
        onDownloadComplete(downloadComplete);
        outputStream.flush();
        outputStream.close();
        inputStream.close();
 
    }
 
 
    private void updateNotification(int currentProgress) {
 
        notificationBuilder.setProgress(100, currentProgress, false);
        notificationBuilder.setContentText(currentProgress + "%");
        notificationManager.notify(0notificationBuilder.build());
    }
 
 
    private void sendProgressUpdate(boolean downloadComplete) {
 
        Intent intent = new Intent(PROGRESS_UPDATE);
        intent.putExtra("downloadComplete", downloadComplete);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }
 
    private void onDownloadComplete(boolean downloadComplete) {
        sendProgressUpdate(downloadComplete);
        String message;
        if (downloadComplete) {
            message = "다운로드가 완료되었습니다.";
        } else {
            message = "다운로드에 실패하였습니다.";
        }
        notificationManager.cancel(0);
        notificationBuilder.setProgress(00false);
        notificationBuilder.setContentText(message);
        notificationManager.notify(0notificationBuilder.build());
    }
 
 
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        notificationManager.cancel(0);
    }
 
"
 

4. MainActivity정의

이부분에서 가장 많이 삽질을 하였는데 파일 다운로드 후 설치 화면을 띄우는 과정에서 FLAG_ACTIVITY_CLEAR_TOP 이 아닌 FLAG_ACTIVITY_NEW_TASK를 지정하였더니 '패키지를 파싱하는 중 문제가 발생했습니다' 라는 오류가 나와서 오류찾는데에 애를 먹었습니다. 

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
 
 
public class MainActivity extends BaseView {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button=findViewById(R.id.but);
 
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                requestPermission();
            }
        });
 
        registerReceiver();
 
    }
 
    private BroadcastReceiver onDownloadReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, Intent intent) {
            if(intent.getAction().equals(DownloadNotificationService.PROGRESS_UPDATE)){
                boolean complete=intent.getBooleanExtra("downloadComplete",false);
                if(complete){
                    Toast.makeText(MainActivity.this,"completed",Toast.LENGTH_SHORT).show();
 
                    File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath() + File.separator +
                            "your.apk");
                    Uri apkUri;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                         apkUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
                    }else{
                        apkUri=Uri.fromFile(file);
                    }
                    Intent openFileIntent = new Intent(Intent.ACTION_VIEW);
                    openFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    openFileIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    openFileIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                    startActivity(openFileIntent);
                    finish();
 
                }
 
            }
        }
    };
 
    private void registerReceiver(){
        LocalBroadcastManager manager=LocalBroadcastManager.getInstance(this);
        IntentFilter intentFilter=new IntentFilter();
        intentFilter.addAction(DownloadNotificationService.PROGRESS_UPDATE);
        manager.registerReceiver(onDownloadReceiver,intentFilter);
    }
 
 
    private void startFileDownload(){
        Intent intent=new Intent(this, DownloadNotificationService.class);
        startService(intent);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        LocalBroadcastManager manager=LocalBroadcastManager.getInstance(this);
        manager.unregisterReceiver(onDownloadReceiver);
    }
 
    private void requestPermission(){
        ActivityCompat.requestPermissions(this,new String[]{
                Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
    }
 
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length > 0 && grantResults[0== PackageManager.PERMISSION_GRANTED) {
 
                    startFileDownload();
                } else {
 
                    Toast.makeText(getApplicationContext(), "Permission Denied", Toast.LENGTH_SHORT).show();
 
                }
                break;
        }
 
    }
}
 
 

 

 

내용이 도움이 되셨거나 초보 블로거를 응원하고 싶으신 분은 아래 하트♥공감 버튼을 꾹 눌러주세요! 

내용의 수정이 있거나 도움이 필요하신 분은 댓글을 남겨주세요!