1. 程式人生 > >安卓專案實戰之APP版本升級更新,適配安卓7.0

安卓專案實戰之APP版本升級更新,適配安卓7.0

前言

APP的版本升級主要分為兩種方式:

1.應用市場升級
2.應用內升級

而應用內升級的方式是目前大多數APP採用的升級更新方式。

應用內升級的模式

按照不同的業務需求又可以分為兩種:
1,強制性更新
如果APP有更新,那麼則彈出更新提示對話方塊,並且該對話方塊上只有一個升級按鈕,並且按返回鍵該對話方塊不會消失,使用者想要繼續使用APP,必須等完成升級覆蓋安裝之後。
2,選擇性更新
如果APP有更新,那麼則彈出更新提示對話方塊,並且該對話方塊上同時包含升級按鈕和暫不升級按鈕。

APP的更新方式

1,APP內下載更新
這時我們必須等下載安裝完全後才能進行操作
2,通知欄下載更新


這種情況是不在應用內更新,放在通知欄並不會影響當前app的使用

APP的下載方式

Android中下載的方式很多種:HttpUrlConnection,Retrofit,okHttp,以及android原生的下載工具類DownLoadManager 等等。本篇我們採用的方式是Google推薦的下載工具類DownLoadManager。

APP升級更新流程

1,獲取當前應用的版本號
2,獲取伺服器端的apk的版本號
3,對比兩個版本號,如果伺服器端版本號大於當前版本號,說明有新版本,則彈出更新對話方塊
4,點選立即更新按鈕,開始更新安裝

使用系統DownloadManager在通知欄更新下載

1.請求介面,獲取伺服器端的json格式的新版本資訊

private void JudgeVersion() {
            String path = GlobalUrl.BASE_URL + GlobalUrl.VERSION_UPDATE;
            RequestParams params = new RequestParams(path);
            x.http().get(params, new Callback.CommonCallback<String>() {
                @Override
                public void onSuccess(String result) {
//  {"versionName":"asdfasfasddf","versionCode":123,"versionSize":"123M","versionDesc":"<p>123123123<\/p>","downloadUrl":"upload\/apk\/20170509\/17101494317003.apk"}
                   //將json字串轉換為Bean物件
                    versionBean = new Gson().fromJson(result, VersionBean.class);
                    // 獲取當前應用的版本號
                    int versionCode = AppUtils.getVersionCode(mActivity);
                    // 比較版本號
                    if(versionBean.getVersionCode() > versionCode && versionBean.getVersionCode() != PrefUtils.getInt(mActivity,"ignoreVersion",0)){
                        // 說明有新版本存在,彈出版本升級提示對話方塊
                        showUpdateDialog();
                    }
                }

                @Override
                public void onError(Throwable ex, boolean isOnCallback) {
//                    Toast.makeText(mActivity, "版本檢測失敗,請稍後重試!", Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onCancelled(CancelledException cex) {

                }

                @Override
                public void onFinished() {

                }
            });
    }

2,versionBean的程式碼如下:

public class VersionBean {

    private String versionName; // 版本名稱
    private int versionCode;    // 版本號
    private String versionSize; //版本大小
    private String versionDesc; //版本描述
    private String downloadUrl; //下載地址

    // 各個屬性對應的getter和setter方法
    
}

3,AppUtils工具類程式碼:

public class AppUtils {
    /**
     * 獲取版本名稱
     *
     * @return
     */
    public static String getVersionName(Context context) {
        PackageManager packageManager = context.getPackageManager();
        try {
            PackageInfo packageInfo = packageManager.getPackageInfo(
                    context.getPackageName(), 0);// 獲取包的資訊

            int versionCode = packageInfo.versionCode;
            String versionName = packageInfo.versionName;

            System.out.println("versionName=" + versionName + ";versionCode="
                    + versionCode);

            return versionName;
        } catch (PackageManager.NameNotFoundException e) {
            // 沒有找到包名的時候會走此異常
            e.printStackTrace();
        }

        return "";
    }

    /**
     * 獲取app的版本號
     *
     * @return
     */
    public static int getVersionCode(Context context) {
        PackageManager packageManager = context.getPackageManager();
        try {
            PackageInfo packageInfo = packageManager.getPackageInfo(
                    context.getPackageName(), 0);// 獲取包的資訊

            int versionCode = packageInfo.versionCode;
            return versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            // 沒有找到包名的時候會走此異常
            e.printStackTrace();
        }

        return -1;
    }
}

DisplayUtil 程式碼:

public class DisplayUtil {

    public static int dp2px(Context context, float dipValue){
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int)(dipValue * scale + 0.5f);
    }
    public static int px2dp(Context context, float pxValue){
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int)(pxValue / scale + 0.5f);
    }

    /**
     * 將px值轉換為sp值,保證文字大小不變
     *
     * @param pxValue
     * @param fontScale
     *            (DisplayMetrics類中屬性scaledDensity)
     * @return
     */
    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }


    public static int sp2px(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue * fontScale + 0.5f);
    }
}

4,彈出更新對話方塊的方法如下:

// 彈出版本更新對話方塊
    private void showUpdateDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
        View v = View.inflate(mActivity, R.layout.update_view, null);
        TextView tvNum1 = (TextView) v.findViewById(R.id.tvNum1);
        TextView tvNum2 = (TextView) v.findViewById(R.id.tvNum2);
        TextView tvNum3 = (TextView) v.findViewById(R.id.tvNum3);
        CheckBox ignore = (CheckBox) v.findViewById(R.id.ignore);
        // 為忽略此版本新增選中監聽
        ignore.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                // 獲取到當前的版本號,此處選擇儲存版本號而非版本名稱是因為判斷是否有新版本時使用的是版本號來進行比較,儲存在SharedPreference裡面
                if(b){
                    // 被選中
                    // 儲存當前版本號
                    PrefUtils.setInt(mActivity,"ignoreVersion",versionBean.getVersionCode());
                }else{
                    // 取消選中
                    PrefUtils.setInt(mActivity,"ignoreVersion",0);
                }
            }
        });
        TextView tvDesc = (TextView) v.findViewById(R.id.tvDesc);
        tvNum1.setText("v"+AppUtils.getVersionName(mActivity));
        tvNum2.setText(versionBean.getVersionName());
        tvNum3.setText(versionBean.getVersionSize());
        tvDesc.setText(Html.fromHtml(versionBean.getVersionDesc()));  // 顯示帶html標籤的更新描述
        TextView tvCancel = (TextView) v.findViewById(R.id.tvCancel);
        TextView tvOk = (TextView) v.findViewById(R.id.tvOk);
        tvCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dialog.dismiss();
            }
        });
        tvOk.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dialog.dismiss();
                dialog = null;
                // 執行下載更新,建議將工具類的呼叫邏輯封裝進一個方法,然後需要新增6.0的SD卡寫入許可權,此處省略。。。
                // 呼叫下載工具類
                DownLoadUtils downloadUtils =   new DownLoadUtils(getContext());
                        downloadUtils.downloadAPK("http://ip地址:埠/服務名/resources/app/app-release.apk", "smartonet.apk");
            }
        });
        builder.setView(v, 0, 0, 0, 0); // 設定內容試圖並去除邊框
        builder.setCancelable(false);
        dialog = builder.create();
        dialog.show();
        // 設定AlertDialog的寬高
        WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
        params.width = DisplayUtil.dp2px(mActivity,350);
        params.height = DisplayUtil.dp2px(mActivity,345);
        dialog.getWindow().setAttributes(params);
    }

5,dialog的自定義View的佈局程式碼:

<?xml version="1.0" encoding="utf-8"?>
<!--aa-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="15dp"
    android:paddingTop="15dp"
    android:paddingRight="15dp"
    android:paddingBottom="10dp"
    android:background="@mipmap/update_bg"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="17sp"
        android:paddingBottom="13dp"
        android:textColor="#000"
        android:layout_gravity="center_horizontal"
        android:text="發現新版本"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:visibility="gone"
        android:layout_marginBottom="10dp"
        android:background="#eeeff3"></View>
    <RelativeLayout
        android:layout_width="match_parent"
        android:paddingBottom="5dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp">
    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:textSize="14sp"
        android:visibility="gone"
        android:text="當前版本:"/>
    <TextView
        android:id="@+id/tvNum1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tv1"
        android:textSize="14sp"
        android:visibility="gone"
        android:text="v0.1"/>
    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tvNum1"
        android:textSize="15sp"
        android:textColor="#474747"
        android:text="最新版本:"/>
    <TextView
        android:id="@+id/tvNum2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tv2"
        android:textColor="#474747"
        android:textSize="15sp"
        android:text="v0.2"/>
    <TextView
        android:id="@id/tv3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv2"
        android:textSize="15sp"
        android:paddingTop="3dp"
        android:textColor="#474747"
        android:paddingBottom="3dp"
        android:text="新版本大小:"/>
    <TextView
        android:id="@+id/tvNum3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv2"
        android:layout_toRightOf="@id/tv3"
        android:paddingTop="3dp"
        android:paddingBottom="10dp"
        android:textColor="#474747"
        android:textSize="15sp"
        android:text="40.0M"/>
    </RelativeLayout>
    <!--<WebView
        android:id="@+id/webView"
        android:layout_below="@id/tvNum3"
        android:layout_marginTop="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </WebView>-->

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:scrollbars="none"
        >
        <RelativeLayout
            android:layout_width="match_parent"
            android:paddingBottom="10dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="6dp">
            <TextView
                android:id="@+id/tvDesc"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#000"
                android:textSize="15sp"
                android:lineSpacingExtra="4dp"
                android:text="更新描述"/>
        </RelativeLayout>
    </ScrollView>
    <CheckBox
        android:id="@+id/ignore"
        style="@style/mycheckbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="13sp"
        android:layout_marginLeft="5dp"
        android:padding="5dp"
        android:textColor="#474747"
        android:layout_marginTop="15dp"
        android:layout_marginBottom="5dp"
        android:text="忽略此版本"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#eeeff3"></View>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_gravity="right"
        android:orientation="horizontal"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tvCancel"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="#000"
            android:gravity="center"
            android:paddingTop="10dp"
            android:clickable="true"
            android:text="以後再說"/>
        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:background="#eeeff3"></View>
        <TextView
            android:id="@+id/tvOk"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:paddingTop="10dp"
            android:layout_height="match_parent"
            android:textSize="16sp"
            android:textColor="@color/title"
            android:gravity="center"
            android:clickable="true"
            android:text="立即更新"/>
    </LinearLayout>
</LinearLayout>

6,app版本更新的工具類

public class DownLoadUtils {
    //下載器
    private DownloadManager downloadManager;
    //上下文
    private Context mContext;
    //下載的ID
    private long downloadId;

    public  DownLoadUtils(Context context){
        this.mContext = context;
    }

    //下載apk
    public void downloadAPK(String url, String name) {
        //建立下載任務
         DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
         //設定用於下載時的網路型別,預設任何網路都可以下載,提供的網路常量有:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI
         // 一般不配置該句,以預設任何網路都可以下載
        // request.setAllowedNetworkTypes(Request.NETWORK_WIFI);
        // 設定漫遊狀態下是否可以下載
        request.setAllowedOverRoaming(false);
        // 下載過程和下載完成後通知欄有通知訊息
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE | DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle("新版本Apk");
        request.setDescription("apk正在下載");
        //此句配置不配置都可以
        request.setVisibleInDownloadsUi(true);
        //設定下載的路徑
        //設定檔案的儲存的位置[三種方式],setDestinationInExternalFilesDir、setDestinationInExternalPublicDir、 setDestinationUri等方法用於設定下載檔案的存放路徑,
        //注意如果將下載檔案存放在預設路徑,那麼在空間不足的情況下系統會將檔案刪除,所以使用上述方法設定檔案存放目錄是十分必要的。
        //第一種 //file:///storage/emulated/0/Android/data/your-package/files/Download/update.apk
        //request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, name); 
        //第二種 //file:///storage/emulated/0/Download/update.apk
        //request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, name); 
        //第三種 自定義檔案路徑 
        //request.setDestinationUri()
        // 此處使用該句設定儲存路徑
        request.setDestinationInExternalPublicDir(Environment.getExternalStorageDirectory().getAbsolutePath() , name);
        //獲取DownloadManager
        downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        //將下載請求加入下載佇列,加入下載佇列後會給該任務返回一個long型的id,通過該id可以取消任務,重啟任務、獲取下載的檔案等等
        downloadId = downloadManager.enqueue(request);
        //註冊廣播接收者,監聽下載狀態
        mContext.registerReceiver(receiver,
                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

    //廣播監聽下載的各個狀態
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            checkStatus(context,intent);
        }
    };


    //檢查下載狀態
    private void checkStatus(Context context, Intent intent) {
        DownloadManager.Query query = new DownloadManager.Query();
        //通過下載的id查詢
        query.setFilterById(downloadId);
        Cursor c = downloadManager.query(query);
        if (c.moveToFirst()) {
            int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
            switch (status) {
                //下載暫停
                case DownloadManager.STATUS_PAUSED:
                    break;
                //下載延遲
                case DownloadManager.STATUS_PENDING:
                    break;
                //正在下載
                case DownloadManager.STATUS_RUNNING:
                    break;
                //下載完成
                case DownloadManager.STATUS_SUCCESSFUL:
                    //下載完成安裝APK
                    installAPK(context,intent);
                    break;
                //下載失敗
                case DownloadManager.STATUS_FAILED:
                    Toast.makeText(mContext, "下載失敗", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
        c.close();
    }

    //下載到本地後執行安裝
    private void installAPK(Context context, Intent intent) {
        //獲取下載檔案的Uri
        Uri downloadFileUri = downloadManager.getUriForDownloadedFile(downloadId);
        if (downloadFileUri != null) {
            Intent intent= new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(downloadFileUri, "application/vnd.android.package-archive");
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
            mContext.unregisterReceiver(receiver);
        }
    }

}

為了適配安卓7.0,將上面的installAPK()方法修改如下:

//下載到本地後執行安裝
    private void installAPK(Context context, Intent intent) {
        long longExtra = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                if (id == longExtra){
//                    Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
                    Intent install = new Intent(Intent.ACTION_VIEW);
                    File apkFile = getExternalFilesDir("DownLoad/jiaogeyi.apk");
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                        install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                        Uri uriForFile = FileProvider.getUriForFile(context, "com.example.administrator.updateapkdemo.fileprovider", apkFile);
                        install.setDataAndType(uriForFile,"application/vnd.android.package-archive");
                    }else {
                        install.setDataAndType(Uri.fromFile(apkFile),"application/vnd.android.package-archive");
                    }
 
                    install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    mContext.startActivity(install);
                    mContext.unregisterReceiver(receiver);
                }
    }

或者直接修改工具類中的程式碼如下:去掉檢查更新狀態的那部分程式碼。

//廣播監聽下載的各個狀態
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            long longExtra = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                if (id == longExtra){
//                    Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
                    Intent install = new Intent(Intent.ACTION_VIEW);
                    File apkFile = getExternalFilesDir("DownLoad/jiaogeyi.apk");
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                        install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                        Uri uriForFile = FileProvider.getUriForFile(context, "com.example.administrator.updateapkdemo.fileprovider", apkFile);
                        install.setDataAndType(uriForFile,"application/vnd.android.package-archive");
                    }else {
                        install.setDataAndType(Uri.fromFile(apkFile),"application/vnd.android.package-archive");
                    }
 
                    install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    mContext.startActivity(install);
                    mContext.unregisterReceiver(receiver);
                }
        }
    };

Android 7.0 的檔案許可權變得尤為嚴格,所以之前的一些程式碼在高一點的系統可能導致崩潰,如果不做版本判斷,在7.0的手機就會丟擲FileUriExposedException異常,說app不能訪問你的app以外的資源。官方文件建議的做法,是用FileProvider來實現檔案共享,即使用FileProvider來間接的獲取檔案。也就是說在你專案的src/res新建個xml資料夾再自定義一個檔案,並在清單檔案裡面進行相關的配置。

這裡首先需要做的是在manifests中宣告FileProvider:

<provider 
    android:name="android.support.v4.content.FileProvider" 
    android:authorities="com.example.administrator.updateapkdemo.fileprovider" 
    /**注意引數統一性 FileProvider.getUriForFile(appContext, "com.example.administrator.updateapkdemo.fileprovider", apkFile);*/ 
    android:exported="false" 
    android:grantUriPermissions="true"> 
    <meta-data 
        android:name="android.support.FILE_PROVIDER_PATHS" 
        android:resource="@xml/file_paths" /> 
</provider>

其中authorities是需要注意的地方,這個屬性的值最好讓他獨一無二,所以我這裡採用包名加fileprovider來設定,你如果非要設定成xxx.ooo也不是不可以,但是必須和以下這行程式碼中的第二個引數一致:

Uri uriForFile = FileProvider.getUriForFile(context, "com.example.administrator.updateapkdemo.fileprovider", apkFile);

其次就是在res下的xml資料夾下建立file_paths.xml檔案:
在這裡插入圖片描述其中file_paths.xml程式碼如下:

<?xml version="1.0" encoding="utf-8"?> 
<paths> 
    <!--path:需要臨時授權訪問的路徑(.代表所有路徑) name:就是你給這個訪問路徑起個名字--> 
    <external-path
        name="external_files"
        path="." /> 
</paths>

當然還有在清單中宣告一些許可權:
在這裡插入圖片描述即可。

使用okhttp,retrofit等下載apk到SD卡指定位置,下載完成後執行自動安裝

1,首先判斷SD卡是否可用

if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    // SD卡可用
} else {
    Toast.makeText(MainActivity.this,"SD卡不可用,請檢查SD卡",Toast.LENGTH_LONG).show();
}

2,執行網路下載操作,將伺服器apk檔案下載儲存在sd卡的指定位置
3,執行自動安裝,適配android 7.0

Intent intent = new Intent(Intent.ACTION_VIEW);

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的許可權問題
          intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
          String authority = getApplicationContext().getPackageName() + ".fileProvider";
          Uri contentUri = FileProvider.getUriForFile(MainActivity.this, authority,       new File(Environment.getExternalStorageDirectory(), "軟體名.apk"));//注意修改
          intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
       } else {
          intent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "軟體名.apk")), "application/vnd.android.package-archive");
          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
       }
       
 startActivity(intent);
 //彈出安裝視窗把原程式關閉,避免安裝完畢點選開啟時沒反應
 killProcess(android.os.Process.myPid());

最後別忘了android7.0適配的配置,配置方式和上面一致。