实现安卓APP内安装更新的功能

1,217 阅读3分钟

我的第一个安卓应用终于也有了APP内安装更新的功能(赶上末班车了吗),记录一些方方面面的关键点。

请添加图片描述

托管检测更新和下载服务

由于没有服务器,这两个核心功能可以托管到一些比较好的平台。检测我用的是蒲公英分发(内测阶段,第二天就给你重置了),下载用的则是***云(hhh)。如果蒲公英过审了也可以只用一个,不知道难度大不大……

图中正在测试,所以没有实际访问人家的api,而是用了缓存。下载是真的,不过url是局域网内服务器上的文件。实际情况下,延迟会更加明显。

安装apk

高版本需要fileprovider,其实不用的话直接vmpolicy微调一下也行。

两个关键点都需要在清单文件中处理:1、定义 fileprovider,2:声明权限(否则没有反应)。

manifest:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

<application ...>

	<provider
		android:name="androidx.core.content.FileProvider"
		android:authorities="${applicationId}.fileprovider"
		android:grantUriPermissions="true"
		android:exported="false">
		<meta-data
			android:name="android.support.FILE_PROVIDER_PATHS"
			android:resource="@xml/file_paths"
			/>
	</provider>
</application>

其中 android:resource="@xml/file_paths"需要提供一个清单文件res/xml/file_paths.xml,可如下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-cache-path path="apks/" name="apks"/>
</paths>

这样,需要下载安装包到: 外部储存->临时文件夹( /storage/0/Android/data/包名/cache )中的 apks 文件夹: File target = new File(getExternalCacheDir(), "apks/"+versionName); ,才能被 FileProvider 识别。

Java 调用 :

	private void startUpdateInstall(File target) {
		try {
			Intent intent = new Intent(Intent.ACTION_VIEW);
			File file = target;
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
				Uri apkUri = FileProvider.getUriForFile(PDICMainActivity.this, "包名.fileprovider", file);
				intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
				intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
			} else {
				intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				Uri uri = Uri.fromFile(file);
				intent.setDataAndType(uri, "application/vnd.android.package-archive");
			}
			startActivity(intent);
		} catch (Exception e) {
			CMN.debug(e);
		}
	}

FileProvider还可以包含更多文件夹, 参考 Stack Overflow - 'Failed to find configured root'

  • <files-path/> --> Context.getFilesDir()
  • <cache-path/> --> Context.getCacheDir()
  • <external-path/> --> Environment.getExternalStorageDirectory()
  • <external-files-path/> --> Context.getExternalFilesDir(String)
  • <external-cache-path/> --> Context.getExternalCacheDir()
  • <external-media-path/> --> Context.getExternalMediaDirs()

Markdown文本 + 超链接混排,实现优雅界面

每一个版本都可以提炼一些简短的介绍,然后在检测更新的时候一起获取,显示出来。

建议用Markdown格式写更新日志,Markdown 之简洁优雅足以胜任一定的生产力。

有许多开源组件可以展示Markdown,比如io.noties.markwon或者org.commonmark,前者体积较大、更加完善,后者更简单,但只是转换为html,需要再配合Html.fromHtml转换Spannable成才行

而安卓的Textview虽然支持各种图文混排,但有一些bug,比如设置linkedmovement后、再设置文本可选,会导致滚动点击时随机崩溃。

两个办法解决,一是自定义textview,try-catch包绕一些会崩溃的方法如dispatchTouchEvent(不推荐)。二是自定义触摸监听器,在onTouch中自行调用ClickableSpan、UrlSpan、LinkSpan等的的点击方式。

轻松实现进度条

这里进度条参考的是百度第一个博客里的:给progressbar设置drawable和自定义progressbar

purpose_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape android:shape="rectangle">
            <corners android:radius="4dp"/>
            <gradient android:startColor="#EFF3F7"
                android:endColor="#EFF3F7"/>
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <scale android:scaleWidth="100%">
            <shape android:shape="rectangle">
                <corners android:radius="4dp"/>
                <gradient android:angle="45"
                    android:startColor="#42D673"
                    android:endColor="#42D673"/>
            </shape>
        </scale>
    </item>
</layer-list>

细看,里面是两层的layerdrawable,第一层是底色,另一层id/progress则是进度的颜色,不过里面的渐变色似乎没有用到啊。

然后布局代码里给seekbar添加android:progressDrawable:@drawable/purpose_drawable属性即可,非常快啊,简直不讲武德。

大佬说得好,不仅仅要创造 progress,还要创造 purpose,以后就叫做 purposeBar 吧。

Put Together

下载之时,我直接用进度条替换了对话框底部的其中一个按钮(见上图)。这种替换操作用着很爽,我甚至提炼了一个方法 ViewUtils.replaceView ,还有一系列操作原生视图的方法 ……

public static View replaceView(View viewToAdd, View viewToRemove) {
	return replaceView(viewToAdd, viewToRemove, true);
}

public static View replaceView(View viewToAdd, View viewToRemove, boolean layoutParams) {
	ViewGroup.LayoutParams lp = viewToRemove.getLayoutParams();
	ViewGroup vg = (ViewGroup) viewToRemove.getParent();
	if(vg!=null) {
		int idx = vg.indexOfChild(viewToRemove);
		removeView(viewToAdd);
		if (layoutParams) {
			vg.addView(viewToAdd, idx, lp);
		} else {
			vg.addView(viewToAdd, idx);
		}
		removeView(viewToRemove);
	}
	return viewToAdd;
}

public static boolean removeView(View viewToRemove) {
	return removeIfParentBeOrNotBe(viewToRemove, null, false);
}

public static boolean removeIfParentBeOrNotBe(View view, ViewGroup parent, boolean tobe) {
	if(view!=null) {
		ViewParent svp = view.getParent();
		if((parent!=svp) ^ tobe) {
			if(svp!=null) {
				((ViewGroup)svp).removeView(view);
				//CMN.Log("removing from...", svp, view.getParent(), view);
				return view.getParent()==null;
			}
			return true;
		}
	}
	return false;
}

最终应用内更新一共七百多行代码,还是很多的,GitHub都说too big diff了(结果发现是缓存字段太长),哈 哈。

下载功能用了Java 版本的 "WGET",两千多行,很不错,好像是国人写的:github.com/winneryong/…