一、前言
通过Unity的UnityWebRequest类来实现从服务器下载Apk文件;在Android Studio中封装安装应用包接口,导出AAR包供Unity调用,并通过广播接收器监测应用是否安装完成,完成则删除下载的Apk文件;Unity端通过C#脚本调用AAR内部封装的接口。
二、安卓交互功能封装
1、Java类创建
(1)创建方法调用类
此类用于创建供Unity端调用的方法,具体代码如下:
import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Build; import androidx.core.content.FileProvider; import java.io.File; public class InstallApkUtils { private Context mContext; private static InstallApkUtils mInstallApkUtils = null; private InitializeApkBroadcastReceiver apkBroadcastReceiver; public static String apkFilePath; public InstallApkUtils(Context context) { this.mContext = context; } public static InstallApkUtils getInstance(Context context){ if (mInstallApkUtils == null) { mInstallApkUtils = new InstallApkUtils(context); } return mInstallApkUtils; } //安装Apk public void installApk(String filePath){ apkFilePath = filePath; File apkFile = new File(filePath); if(!apkFile.exists()){ return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){//安卓7.0以上 Uri apkUri = FileProvider.getUriForFile(mContext,mContext.getPackageName() + ".fileProvider",apkFile); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(apkUri,"application/vnd.android.package-archive"); } else {//安卓7.0以下 intent.setDataAndType(Uri.parse("file://" + apkFile.toString()),"application/vnd.android.package-archive"); } if(mContext.getPackageManager().queryIntentActivities(intent,0).size() > 0){ mContext.startActivity(intent); } } //注册广播 public void registerBroadcast(){ if(apkBroadcastReceiver == null){ apkBroadcastReceiver = new InitializeApkBroadcastReceiver(); } IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); intentFilter.addDataScheme("package"); mContext.registerReceiver(apkBroadcastReceiver,intentFilter); } //取消注册广播 public void unregisterBroadcast(){ if(apkBroadcastReceiver != null){ mContext.unregisterReceiver(apkBroadcastReceiver); apkBroadcastReceiver = null; } } //删除apk文件 public static void removeApkFile(String path){ File apkFile = new File(path); if(apkFile.isFile() && apkFile.exists()){ apkFile.delete(); } } }
(2)创建广播接收器
广播接收器用于监测应用安装包是否安装完成,以便删除安装包。
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class InitializeApkBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())){ InstallApkUtils.removeApkFile(InstallApkUtils.apkFilePath); } if(Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())){ } if(Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())){ } } }
2、AndroidManifest.xml文件配置
添加权限及配置provider节点。广播的注册不在此文件里静态注册,采用代码动态注册,因为导出的是插件包,无法有效针对应用进行注册。
(1)添加权限
<uses-permission android:name="android.permission.REPLACE_EXISTING_PACKAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
(2)配置provider节点
<application> <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/file_paths"/> </provider> </application>
此处,需要保证android:authorities的值与InstallApkUtils类中的mContext.getPackageName() + ".fileProvider"的值保持一致,采用${applicationId}.fileProvider即可保证一致。
3、新建xml资源文件
在模块-->src-->main下新建res文件路径,然后在res下新建xml文件路径,最后在xml文件夹下新建file_paths.xml文件,写入以下内容。
<?xml version="1.0" encoding="utf-8" ?> <paths> <external-path name="apkFiles" path="."/> </paths>
4、构建AAR包
选中模块,点击Build-->Make Module,或者直接Build-->Rebuild Project。
待编译完成后,在模块-->build-->outputs-->aar里便可找到编译好的AAR包。
三、Unity调用
1、AAR包放置
直接将AAR包拖放至Unity的Assets-->Plugins-->Android路径下。
2、创建C#脚本,调用AAR包
(1)从服务器下载方法
using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class ApplicationStoreManager : MonoBehaviour { //回调 public delegate void DownloadCallBack(); public delegate void DownloadCallBack<in T>(T arg); public Slider downloadProgressSlider;//下载进度条 public Text downloadProgressText;//下载进度显示文本 public Button downloadButton;//下载按钮 // Start is called before the first frame update void Start() { DownloadButtonAddListener(); } /// <summary> /// 从服务器下载文件 /// </summary> /// <param name="url"> 文件地址 </param> /// <param name="fileName"> 文件名 </param> /// <param name="downloadCallBack"> 回调函数 </param> private void DownloadFileFromServer(string url,string fileName,DownloadCallBack<string> downloadCallBack) { UnityWebRequest request = UnityWebRequest.Get(url); StartCoroutine(DownloadFileFromServer(request, fileName, downloadCallBack)); } /// <summary> /// 从服务器下载文件协程 /// </summary> /// <param name="unityWebRequest"> UnityWebRequest </param> /// <param name="fileName"> 文件名 </param> /// <param name="downloadCallBack"> 回调函数 </param> /// <returns></returns> IEnumerator DownloadFileFromServer(UnityWebRequest unityWebRequest,string fileName,DownloadCallBack<string> downloadCallBack) { unityWebRequest.SendWebRequest(); if (unityWebRequest.result == UnityWebRequest.Result.ConnectionError) { Debug.Log("Download Error:" + unityWebRequest.error); } else { //下载进度条显示 while (!unityWebRequest.isDone) { downloadProgressSlider.value = unityWebRequest.downloadProgress; //下载进度转化为百分比数值后保留一位小数 float progress = Mathf.Round((unityWebRequest.downloadProgress * 100f) * 10f) / 10f; downloadProgressText.text = progress.ToString() + "%"; yield return 0; } if (unityWebRequest.isDone) { downloadProgressSlider.value = 1; downloadProgressText.text = "100%"; byte[] results = unityWebRequest.downloadHandler.data; string pathUrl = Application.persistentDataPath + "/ApkFile"; //保存文件 SavaFile(results, pathUrl, fileName, downloadCallBack); //取消下载按钮事件 downloadButton.GetComponentInChildren<Text>().text = "下载完成"; downloadButton.onClick.RemoveAllListeners(); } } } /// <summary> /// 保存文件 /// </summary> /// <param name="results"> 下载得到的数据 </param> /// <param name="savePath"> 保存路径 </param> /// <param name="fileName"> 文件名 </param> /// <param name="downloadCallBack"> 回调函数 </param> private void SavaFile(byte[] results,string savePath,string fileName,DownloadCallBack<string> downloadCallBack) { if (!File.Exists(savePath)) { Directory.CreateDirectory(savePath); } string path = savePath + "/" + fileName; FileInfo fileInfo = new FileInfo(path); Stream stream; stream = fileInfo.Create(); stream.Write(results, 0, results.Length); stream.Close(); stream.Dispose(); downloadCallBack(path); } /// <summary> /// 下载按钮添加事件 /// </summary> public void DownloadButtonAddListener() { downloadButton.onClick.AddListener(delegate () { DownloadFileFromServer(); }); } /// <summary> /// 从服务器下载文件 /// </summary> public void DownloadFileFromServer() { downloadButton.GetComponentInChildren<Text>().text = "下载中..."; DownloadFileFromServer("http://IP地址及端口/Apk安装包名称.apk", "Apk安装包名称.apk", InstallApk); } }
(2)安卓接口调用方法
在以上ApplicationStoreManager脚本中增加以下内容。
using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class ApplicationStoreManager : MonoBehaviour { private AndroidJavaObject installApkUtils; private void Awake() { AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); AndroidJavaClass installApkUtilsClass = new AndroidJavaClass("包名.InstallApkUtils"); installApkUtils = installApkUtilsClass.CallStatic<AndroidJavaObject>("getInstance", currentActivity); } // Start is called before the first frame update void Start() { installApkUtils.Call("registerBroadcast"); } private void OnDestroy() { installApkUtils.Call("unregisterBroadcast"); } /// <summary> /// 安装Apk /// </summary> /// <param name="filePath"> Apk文件路径 </param> public void InstallApk(string filePath) { installApkUtils.Call("installApk", filePath); } }
在Start方法里注册广播接收器,在OnDestroy方法里取消注册广播接收器。InstallApk方法用于调用安装Apk方法,其作为下载完成的回调,下载完成后自动调用安装。
3、Http文件服务器
本项目采用HFS用来创建文件服务器,来测试从网络下载文件。详见:HFS官网
HFS下载地址:Download
下载后只有一个exe文件,为免安装文件,将其放于合适的路径下便可。
打开后将需要下载的文件拖放至Home内,也可以直接将整个文件保存文件夹拖入,选中要下载的文件,窗口上方显示的地址即为下载地址。
4、Unity工程设置
此处至关重要,不设置会导致无法使用安卓的androidx。
(1)勾选Publishing Settings内部选项
(2)修改配置文件
勾选上以上两个选项后,会在Unity工程的Assets-->Plugins-->Android文件夹内生成gradleTemplate.properties和mainTemplate.gradle两个文件,需要分别修改这两个文件。
1)修改mainTemplate.gradle文件
在dependencies 块中添加一行代码: implementation 'androidx.appcompat:appcompat:1.2.0’。
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.2.0' **DEPS**}
2)修改gradleTemplate.properties文件
在文件最后面添加以下内容:
android.overridePathCheck=true android.useAndroidX=true android.enableJetifier=true
5、UGUI设计
在Unity编辑器里创建一个Button、Slider及Text,分别用来绑定下载方法、展示下载进度条及展示下载进度百分比。
6、切换至安卓平台,打包apk,安装测试便可
四、AAR包资源下载入口
如果有小伙伴看了以上教程还不知道如何使用,可以直接下载以下AAR包直接使用,里面有插件的包名,替换掉ApplicationStoreManager脚本里的“包名”便可。