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
도움이 되셨다면 댓글 달아주세요. 좋은 글 작성에 큰 힘이 됩니다.