728x90

Last Updated 2020.6.26


테스트 환경 : 삼성 갤럭시노트9 (AndroidQ)


안드로이드폰의 운영체제를 업그레이드하여 8.0 이다. (9.0 이상 사용폰은 마지막에 수정된 부분만 별도 언급)

API 23까지는 APK 파일을 서버에서 다운로드받고 자동으로 설치하는 화면을 띄우는 코드는

Intent intent = new Intent(Intent.ACTION_VIEW);
 Uri apkUri = Uri.fromFile(outputFile);
 intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 context.startActivity(intent);


Android 7.0(Nougat) 이상에서는 위 코드로 실행하면 에러가 발생한다.

앱 외부에서 file:// URI의 노출을 금지하는 StrictMode API 정책을 적용한다.
파일 URI를 포함하는 인텐트가 앱을 떠나면 FileUriExposedException 예외와 함께 앱에 오류가 발생한다.


애플리케이션 간에 파일을 공유하려면 content:// URI를 보내고 이 URI에 대해 임시 액세스 권한을 부여해야 한다.
FileProvider 그 권한을 가장 쉽게 부여하는 방법이다.


구글링해서 찾은 대부분의 자료들이 개념 중심으로 적혀 있어 내용 파악이 쉽지 않더라. 테스트한 코드 핵심 내용을 모두 적었다.


provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="files" path="." />
</paths>


<files-path name="files" path="." />


File path = getFilesDir();
File file = new File(path, "abc.apk");


파일 경로를 변경해서 테스트 해보니 마찬가지로 잘 동작된다.

https://stackoverflow.com/questions/37074872/android-fileprovider-on-custom-external-storage-folder 참조


<external-path name="download" path="." />

File path = new File(Environment.getExternalStorageDirectory() + "/download");



AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.tistory.link2me.filedownload">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme.NoActionBar"

        android:usesCleartextTraffic="true">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <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/provider_paths"
                tools:replace="android:resource" />
        </provider>

        <activity android:name=".DownloadAPK" />
    </application>

</manifest>



MainActivity.java

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    Context context;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            MainActivity.this.Button_Click();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(MainActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this.getBaseContext();

        if (Build.VERSION.SDK_INT >= 23) {
            TedPermission.with(this)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("파일을 다운로드 하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE"})
                    .check();
        } else {
            Button_Click();
        }

    }

    private void Button_Click() {
        Button btn_dnload = (Button) findViewById(R.id.btn_image_download);
        btn_dnload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadAPK.class);
                startActivity(intent);
            }
        });
    }
}


Value.java

public class Value extends Activity {
    public static final String APKNAME = "ABC.apk"; // APK name
    public static final String IPADDRESS = "http://100.100.100.100";
}



DownloadAPK.java

import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
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 DownloadAPK extends AppCompatActivity {
    Context context;
    private File outputFile;
    ProgressBar progressBar;
    TextView textView;
    LinearLayout linearLayout;
    DownloadFileFromURL downloadFileAsyncTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.downloadapk_activity);
        context = this.getBaseContext();

        linearLayout = (LinearLayout) findViewById(R.id.downloadprogress_layout);
        textView = (TextView) findViewById(R.id.txtView01);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);

        DownloadAPK();
    }

    private void DownloadAPK() {
        // 백그라운드 객체를 만들어 주어야 다운로드 취소가 제대로 동작됨
        downloadFileAsyncTask = new DownloadFileFromURL();
        downloadFileAsyncTask.execute(Value.IPADDRESS + "/Download.php");
    }

    class DownloadFileFromURL extends AsyncTask<String, Integer, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressBar.setProgress(0);
        }

        @Override
        protected String doInBackground(String... apkurl) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;

            try {
                URL url = new URL(apkurl[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

                File path = getFilesDir();
                outputFile = new File(path, Value.APKNAME);
                if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                    outputFile.delete();
                }

                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }

                fos.flush();

            } catch (Exception e) {
                e.printStackTrace();
                Log.e("UpdateAPP", "Update error! " + e.getMessage());
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch(IOException ioex) {
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
            progressBar.setProgress(progress[0]);
            textView.setText("다운로드 : " + progress[0] + "%");
        }

        protected void onPostExecute(String result) {
            if (result == null) {
                progressBar.setProgress(0);
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다.", Toast.LENGTH_LONG).show();

                System.out.println("getPackageName : "+getPackageName());
                System.out.println("APPLICATION_ID Path : "+BuildConfig.APPLICATION_ID);
                System.out.println("outputFile Path : "+ outputFile.getAbsolutePath());
                System.out.println("Fie getPath : "+ outputFile.getPath());

                // 미디어 스캐닝
                MediaScannerConnection.scanFile(getApplicationContext(), new String[]{outputFile.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {

                    }
                });


                // 다운로드한 파일 실행하여 업그레이드 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    // Android Nougat ( 7.0 ) and later
                    installApk(outputFile);
                    System.out.println("SDK_INT 24 이상 ");
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(outputFile);
                    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                    System.out.println("SDK_INT 23 이하 ");
                }

            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }

        protected void onCancelled() {
            // cancel메소드를 호출하면 자동으로 호출되는 메소드
            progressBar.setProgress(0);
            textView.setText("다운로드 진행 취소됨");
        }
    }

    public void installApk(File file) {
        Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        context.startActivity(intent);
    }

}

위 분홍색 installApk() 메서드는 에러가 발생할 것이다.

BuildConfig.APPLICATION_ID 를 제대로 인식하지 못하는 문제더라.


    public void installApk(File file) {
        Uri fileUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
        finish();
    } 

이 코드로 대체하면 정상적으로 앱 업데이트가 이루어진다.


앱 build.gradle

apply plugin: 'com.android.application'
// 코틀린 혼용 사용을 위해 추가
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.abc"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0' // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.journeyapps:zxing-android-embedded:3.6.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.material:material:1.0.0'

    implementation 'com.naver.maps:map-sdk:3.7.1' // 네이버 지도 SDK
    implementation 'com.google.android.gms:play-services-location:17.0.0'
}


안드로이드 8.0 출처를 알 수 없는 앱 설정화면 코드는 http://mixup.tistory.com/78 참조해서 해결했다.


Value.APKNAME 은 "abc.apk" 와 같은 명칭이다.
미디어 스캐닝은 파일 다운로드한 폴더의 내용을 확인할 목적으로 검색하다 알게된 걸 적용해봤다.


참고 사이트

https://pupli.net/2017/06/18/install-apk-files-programmatically/


http://stickyny.tistory.com/110


http://mixup.tistory.com/98


도움이 되셨다면 광고 클릭 해 주세요. 좋은 글 작성에 큰 힘이 됩니다

출처: https://link2me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]

도움이 되셨다면 댓글 달아주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,