正文

升级是应用最基本的功能,因为很少有一个应用发布后不在进行后期维护!
原生应用的升级比较常见,但是如今混合应用大热,因为项目,我就基于ionic框架实现了一个简单的升级,根据服务器端返回来确定强制还是非强制更新.

插件安装

file(访问文件)

ionic cordova plugin add cordova-plugin-file
npm install --save @ionic-native/file

File Transfer(上载和下载文件)

ionic cordova plugin add cordova-plugin-file-transfer
npm install --save @ionic-native/file-transfer

App Version(用来获取版本号)

ionic cordova plugin add cordova-plugin-app-version
npm install --save @ionic-native/app-version

Uid(获取设备标识,主要用于灰度升级,只升级用不到)

ionic cordova plugin add cordova-plugin-uid
npm install --save @ionic-native/uid

File Opener(打开下载完成的apk文件)

ionic cordova plugin add cordova-plugin-file-opener2
npm install --save @ionic-native/file-opener

Android Permissions(获取运行时权限)

ionic cordova plugin add cordova-plugin-android-permissions
npm install --save @ionic-native/android-permissions

升级服务

在src/app文件夹下创建NativeService.ts升级服务.

/**
 * Created by llcn on 11-29.
 * 升级模块
 */
import {Injectable} from '@angular/core';
import {Platform, AlertController} from 'ionic-angular';
import {AppVersion} from '@ionic-native/app-version';
import {File} from '@ionic-native/file';
import {FileTransfer, FileTransferObject} from "@ionic-native/file-transfer";
import {FileOpener} from '@ionic-native/file-opener';
import {Uid} from "@ionic-native/uid";
import {AndroidPermissions} from "@ionic-native/android-permissions";
import {ToastController} from 'ionic-angular';
import {HttpClient} from "@angular/common/http";

@Injectable()
export class NativeService {

  constructor(private http: HttpClient,
              private platform: Platform,
              private alertCtrl: AlertController,
              private transfer: FileTransfer,
              private appVersion: AppVersion,
              private file: File,
              private fileOpener: FileOpener,
              private uid: Uid,
              private toastCtrl: ToastController,
              private androidPermissions: AndroidPermissions) {
  }


  /**
   * 检查app是否需要升级
   */
  detectionUpgrade() {
    //这里连接后台获取app最新版本号,然后与当前app版本号(this.getVersionNumber())对比
    //版本号不一样就需要申请,不需要升级就return

    this.getVersionNumber().then((version) => { // 获取版本号
      this.getImei().then((imei) => { // 获取imei,用于灰度升级,有些需求不需要这一步

        let body = {tag: 'update', data: {type: "chcnav", terminal: imei, version: version}} //参数
        const url = 'xxx.xxx.xxx'; // 接口地址
        
        this.http.get(url).subscribe(res => {
          // 判断版本号
          if (res && ((res as any).status > 0) && ((res as any).data.version !== version)) { 
            let apkUrl =  (res as any).data.path; // apk下载路径
            if ((res as any).data.force_update) { //是否强制升级(有些版本更迭是强制的,所以用户必须安装)
              this.alertCtrl.create({
                title: '升级提示',
                subTitle: '发现新版本,是否立即升级?',
                enableBackdropDismiss: false,
                buttons: [{
                  text: '确定',
                  handler: () => {
                    this.storagePermissions().then(res => {
                      if (res) {
                        this.downloadApp(apkUrl);
                      }
                    })
                  }
                }]
              }).present();
            } else {
              this.alertCtrl.create({
                title: '升级提示',
                subTitle: '发现新版本,是否立即升级?',
                enableBackdropDismiss: false,
                buttons: [{
                  text: '取消'
                }, {
                  text: '确定',
                  handler: () => {
                    // this.downloadApp(apkUrl);
                    this.storagePermissions().then(res => {
                      if (res) {
                        this.downloadApp(apkUrl);
                      }
                    })
                  }
                }]
              }).present();
            }
          }
        }, error => {
        })
      })
    })
  }

  /**
   * 下载安装app
   */
  downloadApp(url: any) {
    let options;
    options = {
      title: '下载进度',
      subTitle: '当前已下载: 0%',
      enableBackdropDismiss: false
    }
    let alert = this.alertCtrl.create(options);
    alert.present();

    const fileTransfer: FileTransferObject = this.transfer.create();
    console.log(this.file.externalRootDirectory)
    const apk = this.file.externalRootDirectory + 'android.apk'; //apk保存的目录

    fileTransfer.download(url, apk).then(() => {
      this.fileOpener.open(apk, 'application/vnd.android.package-archive').then(() => {
      }).catch(e => {
        console.log('Error opening file' + e)
      });
    }).catch(err => {
      // 存储权限出问题
      this.toastCtrl.create({
        message: '存储apk失败,请检查您是否关闭了存储权限!',
        duration: 3000,
        position: 'bottom'
      }).present();
    });

    fileTransfer.onProgress((event: ProgressEvent) => {
      let num = Math.floor(event.loaded / event.total * 100);
      let title = document.getElementsByClassName('alert-sub-title')[0];
      if (num === 100) {
        // alert.dismiss();
        title && (title.innerHTML = '下载完成,请您完成安装');
      } else {
        title && (title.innerHTML = '当前已下载:' + num + '%');
      }
    });

  }


  /**
   * 获得app版本号,如0.01
   * @description  对应/config.xml中version的值
   * @returns {Promise<string>}
   */
  getVersionNumber(): Promise<string> {
    return new Promise((resolve) => {
      this.appVersion.getVersionNumber().then((value: string) => {
        resolve(value);
      }).catch(err => {
        console.log('getVersionNumber:' + err);
      });
    });
  }


  /**
   * 获取imei号
   */
  async getImei() {
    const {hasPermission} = await this.androidPermissions.checkPermission(
      this.androidPermissions.PERMISSION.READ_PHONE_STATE
    );

    if (!hasPermission) {
      const result = await this.androidPermissions.requestPermission(
        this.androidPermissions.PERMISSION.READ_PHONE_STATE
      );

      if (!result.hasPermission) {
        // throw new Error('Permissions required');
        this.platform.exitApp(); // 因为必须,所以被拒绝就退出app
      }

      return;
    }

    return this.uid.IMEI
  }


  /**
   * 存储运行时权限
   * apk下载时请求存储权限
   *
   */
  async storagePermissions() {
    const {hasPermission} = await this.androidPermissions.checkPermission(
      this.androidPermissions.PERMISSION.READ_EXTERNAL_STORAGE
    );

    if (!hasPermission) {
      const result = await this.androidPermissions.requestPermission(
        this.androidPermissions.PERMISSION.READ_EXTERNAL_STORAGE
      );

      if (!result.hasPermission) {
        // throw new Error('存储权限被拒绝');
        this.platform.exitApp(); // 因为必须,所以被拒绝就退出app
      }
      return true;
    }

    return true;
  }

}

使用

在app.module.ts中注入需要的服务

import {File} from "@ionic-native/file";
import {FileTransfer, FileTransferObject} from '@ionic-native/file-transfer';
import {AppVersion} from '@ionic-native/app-version';
import {AndroidPermissions} from '@ionic-native/android-permissions/';
import {Uid} from '@ionic-native/uid';
import {NativeService} from './NativeService'
import {FileOpener} from "@ionic-native/file-opener";
  providers: [
    FileTransfer,
    File,
    NativeService,
    AppVersion,
    Uid,
    AndroidPermissions,
    FileOpener,
    FileTransferObject,
  ]

使用

在app.component.ts使用

 constructor(private nativeService: NativeService,...) {
    platform.ready().then(() => {
      ...
      this.nativeService.detectionUpgrade();
     ...
    });
  }

注意问题

  1. android升级后通过fileOpener打开apk不出现完成打开按钮

原因: fileOpener2插件问题

处理方法:

找到platforms下的Android源码,找到fileOpener的Java类,添加如下代码:

一般该类目录为:io.github.pwlin.cordova.plugins.fileopener2;

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

private void _open(String fileArg, String contentType, Boolean openWithDefault, CallbackContext callbackContext) throws JSONException {
    String fileName = "";
    try {
      CordovaResourceApi resourceApi = webView.getResourceApi();
      Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
      fileName = this.stripFileProtocol(fileUri.toString());
    } catch (Exception e) {
      fileName = fileArg;
    }
    File file = new File(fileName);
    if (file.exists()) {
      try {
        Uri path = Uri.fromFile(file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if ((Build.VERSION.SDK_INT >= 23 && !contentType.equals("application/vnd.android.package-archive")) || ((Build.VERSION.SDK_INT == 24 || Build.VERSION.SDK_INT == 25) && contentType.equals("application/vnd.android.package-archive"))) {
 
          Context context = cordova.getActivity().getApplicationContext();
          path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".opener.provider", file);
          intent.setDataAndType(path, contentType);
          intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
          intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//这里
          //intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
 
          List<ResolveInfo> infoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
          for (ResolveInfo resolveInfo : infoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            context.grantUriPermission(packageName, path, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
          }
        } else {
          intent.setDataAndType(path, contentType);
          intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//这里
        }
        /*
         * @see
         * http://stackoverflow.com/questions/14321376/open-an-activity-from-a-cordovaplugin
         */
        if (openWithDefault) {
          cordova.getActivity().startActivity(intent);
        } else {
          cordova.getActivity().startActivity(Intent.createChooser(intent, "Open File in..."));
        }
 
        callbackContext.success();
      } catch (android.content.ActivityNotFoundException e) {
        JSONObject errorObj = new JSONObject();
        errorObj.put("status", PluginResult.Status.ERROR.ordinal());
        errorObj.put("message", "Activity not found: " + e.getMessage());
        callbackContext.error(errorObj);
      }
    } else {
      JSONObject errorObj = new JSONObject();
      errorObj.put("status", PluginResult.Status.ERROR.ordinal());
      errorObj.put("message", "File not found");
      callbackContext.error(errorObj);
    }
  }

这样修改如果每次重新生成平台都得改,也可以直接修改插件里面.在安装插件时就已经修改.

  1. android8无法自动打开安装程序(权限拒绝)

因为android8的权限问题,apk下载完成后无法正常自动打开安装程序,所以必须将平台targetSdkVersion版本进行修改.

修改latformsandroidappsrcmainAndroidManifest.xml里面targetSdkVersion的值为23.(所以得先添加平台,修改后再编译)

  1. error: resource android:attr/fontVariationSettings resource android:attr/ttcIndex not found.

方案一: 在/platforms/android/build.gradle和/platforms/android/app/build.gradle中添加如下代码.

configurations.all {
    resolutionStrategy {
        force 'com.android.support:support-v4:27.1.0'
    }
}

方案二: 下载(推荐)

安装cordova-android-support-gradle-release插件

ionic cordova plugin add cordova-android-support-gradle-release --fetch

DLLCNX
50 声望6 粉丝

这个世界很大,必须心平气和