先看完成圖(系統Android 7.0, API 24)↓
在這裡練習的APK(此專案的apk檔-VersionCode為2)下載網址為
https://github.com/SolinariWu/AutomaticallyOpenApk/raw/master/Solinari.apk
上一個例子有一個About的頁面,關於此APP的資訊,我們新增一個Update的按鈕。
about.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical"> | |
<include | |
layout="@layout/toolbar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:layout_scrollFlags="scroll|enterAlways|snap" /> | |
<TextView | |
android:id="@+id/textView" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="@string/version" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:textStyle="bold" | |
android:layout_marginTop="5dp" | |
android:layout_marginLeft="5dp"/> | |
<LinearLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="horizontal"> | |
<TextView | |
android:id="@+id/tvVersion" | |
android:layout_width="100dp" | |
android:layout_height="wrap_content" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:layout_marginLeft="5dp" /> | |
<Button | |
android:id="@+id/btnUpload" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="@string/update" /> | |
</LinearLayout> | |
<TextView | |
android:id="@+id/textView2" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="@string/author" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:textStyle="bold" | |
android:layout_marginTop="5dp" | |
android:layout_marginLeft="5dp"/> | |
<TextView | |
android:id="@+id/tvAuthor" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:text="@string/author_name" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:layout_marginLeft="5dp"/> | |
<TextView | |
android:id="@+id/textView3" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="@string/email" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:textStyle="bold" | |
android:layout_marginTop="5dp" | |
android:layout_marginLeft="5dp"/> | |
<TextView | |
android:id="@+id/tvEmail" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:text="@string/author_email" | |
android:textColor="@color/black" | |
android:textSize="18sp" | |
android:layout_marginLeft="5dp"/> | |
</LinearLayout> |
Android Manifest中加入權限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
我們透過DownloadManager這個System Service來進行下載的動作,將路徑設置於外部公開空間(Public external storage),並針對Andriud 6.0以上版本的系統,檢查是否有讀取的權限。
About.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.solinari.automaticallyopenapk; | |
import android.Manifest; | |
import android.app.DownloadManager; | |
import android.content.Context; | |
import android.content.pm.PackageInfo; | |
import android.content.pm.PackageManager; | |
import android.database.ContentObserver; | |
import android.database.Cursor; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.os.Bundle; | |
import android.os.Environment; | |
import android.os.Handler; | |
import android.support.v4.app.ActivityCompat; | |
import android.support.v4.content.ContextCompat; | |
import android.view.View; | |
import android.widget.Button; | |
import android.widget.TextView; | |
import org.w3c.dom.Text; | |
/** | |
* Created by Solinari on 2017/02/15. | |
*/ | |
public class About extends Navigation_BaseActivity { | |
DownloadManager DM ; | |
DownloadManager.Request request; | |
private long LatestDownloadID; | |
String URL; | |
DialogFragmentHelper newFragment; | |
static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads"); | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.about); | |
Button btnUpload = (Button) findViewById(R.id.btnUpload); | |
toolbar.setTitle(R.string.about);//設置ToolBar Title | |
setUpToolBar();//使用父類別的setUpToolBar(),設置ToolBar | |
CurrentMenuItem = 1;//目前Navigation項目位置 | |
NV.getMenu().getItem(CurrentMenuItem).setChecked(true);//設置Navigation目前項目被選取狀態 | |
TextView tvVersion = (TextView) findViewById(R.id.tvVersion); | |
try {//取得APP目前的versionName | |
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); | |
tvVersion.setText( packageInfo.versionName); | |
} catch (PackageManager.NameNotFoundException e) { | |
e.printStackTrace(); | |
} | |
btnUpload.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View view) { | |
DownloadNewVersion(); | |
} | |
}); | |
} | |
private void DownloadNewVersion(){ | |
newFragment = new DialogFragmentHelper(); | |
newFragment.show(getSupportFragmentManager(),"download apk"); | |
DM = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); | |
URL = "https://github.com/SolinariWu/AutomaticallyOpenApk/raw/master/Solinari.apk"; | |
Uri uri = Uri.parse(URL); | |
request = new DownloadManager.Request(uri); | |
request.setMimeType("application/vnd.android.package-archive");//設置MIME為Android APK檔 | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//Android 6.0以上需要判斷使用者是否願意開啟儲存(WRITE_EXTERNAL_STORAGE)的權限 | |
CheckStoragePermission(); | |
} | |
else{ | |
DownloadManagerEnqueue(); | |
} | |
} | |
private void DownloadManagerEnqueue(){ | |
//創建目錄 | |
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ; | |
//設定APK儲存位置 | |
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS , "DG_App.apk" ) ; | |
LatestDownloadID= DM.enqueue(request); | |
} | |
private void CheckStoragePermission() {//Android 6.0檢查是否開啟儲存(WRITE_EXTERNAL_STORAGE)的權限,若否,出現詢問視窗 | |
if (ContextCompat.checkSelfPermission(this, | |
Manifest.permission.WRITE_EXTERNAL_STORAGE) | |
!= PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, | |
Manifest.permission.READ_EXTERNAL_STORAGE) | |
!= PackageManager.PERMISSION_GRANTED) {//Can add more as per requirement | |
ActivityCompat.requestPermissions(this, | |
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE}, | |
20); | |
} else { | |
DownloadManagerEnqueue(); | |
} | |
} | |
@Override//Android 6.0以上 接收使用者是否允許使用儲存權限 | |
public void onRequestPermissionsResult(int requestCode, | |
String permissions[], int[] grantResults) { | |
switch (requestCode) { | |
case 20: { | |
if (grantResults.length > 0 | |
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { | |
DownloadManagerEnqueue(); | |
} else { | |
CheckStoragePermission(); | |
} | |
return; | |
} | |
} | |
} | |
} |
執行效果如下(可以注意到左上角有下載圖示)↓
這時候,當左上角的下載圖示消失時,我們如果至我們設定下載路徑底下可以發現,APK檔已被成功下載下來了,但這距離我想要的效果還差了許多,在下載過程中,我想要使用者能直接一目瞭然目前的下載進度,而不需要拉下提示欄觀看,那要怎麼實現呢?
先設計一個進度對話框,download_apk_dialog.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
android:orientation="vertical" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:background="#ffffffff"> | |
<TextView | |
android:id="@+id/textView" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="@string/update" | |
android:textColor="@color/black" | |
android:textStyle="normal|bold" | |
android:layout_marginTop="15dp" | |
android:layout_marginLeft="15dp"/> | |
<TextView | |
android:id="@+id/textView2" | |
android:layout_below="@id/textView" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginTop="15dp" | |
android:layout_marginLeft="15dp" | |
android:text="@string/wait" /> | |
<ProgressBar | |
android:id="@+id/download_progressBar" | |
android:layout_below="@id/textView2" | |
style="?android:attr/progressBarStyleHorizontal" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:indeterminate="false" | |
android:layout_marginTop="15dp" | |
android:layout_marginLeft="15dp" | |
android:layout_marginRight="15dp" | |
android:max="100" /> | |
<TextView | |
android:id="@+id/txtProgress" | |
android:layout_below="@id/download_progressBar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginLeft="15dp" | |
android:layout_marginBottom="15dp" | |
android:textStyle="normal|bold"/> | |
</RelativeLayout> |
撰寫一個DialogFragmentHelper.java,用來呼叫DialogFragment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.solinari.automaticallyopenapk; | |
import android.os.Bundle; | |
import android.support.annotation.Nullable; | |
import android.support.v4.app.DialogFragment; | |
import android.view.LayoutInflater; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.view.Window; | |
import android.widget.Button; | |
import android.widget.ProgressBar; | |
import android.widget.TextView; | |
/** | |
* Created by Solinari on 2016/12/4. | |
*/ | |
public class DialogFragmentHelper extends DialogFragment{ | |
private ProgressBar progressBar;//下載新版APP時ProgressBar | |
private TextView txtProgress;//下載新版APP時顯示文字百分比 | |
@Nullable | |
@Override | |
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | |
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);//取消Dialog title列 | |
getDialog().setCanceledOnTouchOutside(false);//不能點擊Dialog以外區域 | |
View v = inflater.inflate(R.layout.download_apk_dialog, container, false); | |
progressBar = (ProgressBar) v.findViewById(R.id.download_progressBar); | |
txtProgress = (TextView) v.findViewById(R.id.txtProgress); | |
txtProgress.setText("0%"); | |
return v; | |
} | |
protected void setProgress(int progress){//更新進度條 | |
progressBar.setProgress(progress); | |
txtProgress.setText(Integer.toString(progress) + "%"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DownloadObserver extends ContentObserver | |
{ | |
public DownloadObserver(Handler handler) { | |
super(handler); | |
} | |
@Override | |
public void onChange(boolean selfChange) { | |
DownloadManager.Query query = new DownloadManager.Query(); | |
query.setFilterById(LatestDownloadID); | |
DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); | |
final Cursor cursor = dm.query(query); | |
if (cursor != null && cursor.moveToFirst()) { | |
final int totalColumn = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); | |
final int currentColumn = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); | |
int totalSize = cursor.getInt(totalColumn); | |
int currentSize = cursor.getInt(currentColumn); | |
float percent = (float) currentSize / (float) totalSize; | |
final int progress = Math.round(percent * 100); | |
runOnUiThread(new Runnable() {//確保在UI Thread執行 | |
@Override | |
public void run() { | |
newFragment.setProgress(progress); | |
} | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private void DownloadManagerEnqueue(){ | |
//創建目錄 | |
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ; | |
//設定APK儲存位置 | |
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS , "Solinari.apk" ) ; | |
downloadObserver = new DownloadObserver(null); | |
getContentResolver().registerContentObserver(CONTENT_URI, true, downloadObserver);//註冊ContentObserver | |
LatestDownloadID= DM.enqueue(request); | |
} |
讓我們來看看效果↓
撰寫SharedPreferencesHelper.java
撰寫BroadcastReceiver之前,針對7.0以上版本的手機,在取得APK檔的Uri時,需要透FileProvider,才可以正常運作,先在Android Manifest中加入
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<provider | |
android:name="android.support.v4.content.FileProvider" | |
android:authorities="com.example.solinari.automaticallyopenapk.fileProvider" | |
android:exported="false" | |
android:grantUriPermissions="true" > | |
<meta-data | |
android:name="android.support.FILE_PROVIDER_PATHS" | |
android:resource="@xml/file_paths" /> | |
</provider> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<paths> | |
<external-path path="." name="external_storage" /> | |
</paths> |
撰寫DownloadCompleteReceiver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.solinari.automaticallyopenapk; | |
import android.annotation.SuppressLint; | |
import android.app.DownloadManager; | |
import android.content.BroadcastReceiver; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.database.Cursor; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.support.v4.content.FileProvider; | |
import android.text.TextUtils; | |
import java.io.File; | |
/** | |
* Created by Solinari on 2017/3/2. | |
*/ | |
public class DownloadCompleteReceiver extends BroadcastReceiver { | |
static SharedPreferencesHelper sp ; | |
Context con; | |
public DownloadCompleteReceiver(Context context){ | |
con = context; | |
sp = new SharedPreferencesHelper(context); | |
} | |
@SuppressLint("NewApi") | |
public void onReceive(Context context, Intent intent) { | |
long downLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); | |
long cacheDownLoadId = sp.getDownloadID(); | |
if (cacheDownLoadId == downLoadId) { | |
Intent install = new Intent(Intent.ACTION_VIEW); | |
File apkFile = queryDownloadedApk(context); | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//Android 7.0 需要透過FileProvider來取得APK檔的Uri | |
install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | |
Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", apkFile); | |
install.setDataAndType(contentUri, "application/vnd.android.package-archive"); | |
} else { | |
install.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); | |
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
} | |
context.startActivity(install);//開啟安裝畫面 | |
} | |
} | |
//透過downLoadId尋找下載的apk檔,解决6.0以上版本安裝問題 | |
public static File queryDownloadedApk(Context context) { | |
File targetApkFile = null; | |
DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); | |
long downloadId =sp.getDownloadID(); | |
if (downloadId != -1) { | |
DownloadManager.Query query = new DownloadManager.Query(); | |
query.setFilterById(downloadId); | |
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL); | |
Cursor cur = downloader.query(query); | |
if (cur != null) { | |
if (cur.moveToFirst()) { | |
String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); | |
if (!TextUtils.isEmpty(uriString)) { | |
targetApkFile = new File(Uri.parse(uriString).getPath()); | |
} | |
} | |
cur.close(); | |
} | |
} | |
return targetApkFile; | |
} | |
} | |
最後,在DownloadManagerEnqueue()中註冊BroadcastReceiver並儲存DownloadID
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private void DownloadManagerEnqueue(){ | |
//創建目錄 | |
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ; | |
//設定APK儲存位置 | |
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS , "Solinari.apk" ) ; | |
DownloadCompleteReceiver receiver = new DownloadCompleteReceiver(getApplicationContext()); | |
registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));//註冊DOWNLOAD_COMPLETE-BroadcastReceiver | |
downloadObserver = new DownloadObserver(null); | |
getContentResolver().registerContentObserver(CONTENT_URI, true, downloadObserver);//註冊ContentObserver | |
LatestDownloadID= DM.enqueue(request); | |
SharedPreferencesHelper sp = new SharedPreferencesHelper(getApplicationContext()); | |
sp.setDownloadID(LatestDownloadID);//儲存DownloadID | |
} |
參考文獻:DownloadManager+MaterialDialog下載進度和6.0安裝APK和"程式未安裝"問題
解決 Android N 上 錯誤:android.os.FileUriExposedException
Android 6.0 儲存權限
完整專案:Solinari GitHub