Android 13 监控网络连接状态

Android 13 监控网络连接状态

获取瞬时状态

1
2
3
4
5
6
val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val currentNetwork = cm.activeNetwork
if (currentNetwork != null) {
val caps = cm.getNetworkCapabilities(currentNetwork)
val linkProperties = cm.getLinkProperties(currentNetwork)
}

监听网络事件

NetworkCallback 类与 ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback)ConnectivityManager.registerNetworkCallback(NetworkCallback) 结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.e(TAG, "The default network is now: " + network)
}

override fun onLost(network: Network) {
Log.e(TAG, "The application no longer has a default network. The last default network was " + network)
handle(null)
}

override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
Log.d(TAG, "The default network changed capabilities: " + networkCapabilities)
handle(networkCapabilities)
}

override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
Log.i(TAG, "The default network changed link properties: " + linkProperties)
}
})

解析NetworkCapabilities的网络状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 private fun handle(caps: NetworkCapabilities?) {
if (caps != null) {
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
if (
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
) {
setResult(STATE_WIFI)
return
} else if (
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
) {
setResult(STATE_MOBILE)
return
}
}
}
setResult(STATE_UNKNOWN)
return
}

APK签名之jarsigner签名工具

APK签名之jarsigner签名工具

使用JDK签名工具jarsigner签名APK文件 jarsigner -verbose -keystore [签名文件路径] -signedjar [签名后的apk文件路径] [未签名的apk文件路径] [证书别名]

20230110180748

1
jarsigner -verbose -keystore D:\xxx\xxx.jks -signedjar D:\xxx\xxx_signed.apk D:\xxx\***.apk keyAlias

Android首页灰色实现方案

Activity设置灰色

使用ColorMatrix设置灰度

1
2
3
4
5
6
7
private fun setGrayPaint(view: View) {
val paint = Paint()
val cm = ColorMatrix()
cm.setSaturation(0f)
paint.colorFilter = ColorMatrixColorFilter(cm)
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
}

给首页Activity的decorView设置灰度Paint

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(...)

// 需要在 setContentView 之后
setGrayPaint(window.decorView)
}

需要特殊处理的控件

  • 弹框
  • WebView
  • SurfaceView

这些控件由于不是跟activity公用一个window,需要各自单独处理灰度Paint。调用 setGrayPaint(view) 即可。

相关资源

Android实现设置灰白模式效果

微信AndResGuard资源混淆工具

微信AndResGuard资源混淆工具

AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。

AndResGuard不涉及编译过程,只需输入一个apk(无论签名与否,debug版,release版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的apk(若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的apk)以及对应资源ID的mapping文件。

0x01 原理介绍

根据Android的编译流程,所有资源ID已经被编译成32位int值。这说明我们并不需要去修改xml与java,因为在编译过程已经被R.java所替换,我们直接修改resources.arsc的二进制数据,不改变打包流程,只要在生成resources.arsc之后修改它,同时重命名资源文件。

0x02 使用场景

  • 缩小APK体积
  • 保护res资源文件的可读性
  • 皮应用中减少跟主应用代码的重复率

0x03 资源混淆配置

  1. Project根目录的build.gradle中,添加插件的依赖
1
2
3
4
5
buildscript {
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
}
}
  1. app模块的build.gradle中添加Res相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
plugins {
id 'AndResGuard'
}

android {...}

andResGuard {
mappingFile = file("./resource_mapping.txt")
use7zip = false
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
"R.mipmap.ic_launcher",
"R.mipmap.ic_launcher_round",
"R.string.app_name",
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.21'
//path = "/usr/local/bin/7za"
}

/** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/
// finalApkBackupPath = "${project.rootDir}/final.apk"

/** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/
// digestalg = "SHA-256"
}

0x04 如何启动

使用Android Studio的同学可以在 andresguard 下找到相关的构建任务; 命令行可直接运行./gradlew resguard[BuildType | Flavor], 这里的任务命令规则和assemble一致。

0x05 配置7Zip压缩

在设置sevenzip时, 你只需设置artifact或path, 支持同时设置,总以path的值为优先。

0x06 配置apk输出

如果没有配置finalApkBackupPath,最终结果会覆盖assemble[BuildType | Flavor]的输出APK。如果配置则输出至finalApkBackupPath配置路径。

0x07 Font资源不支持混淆

如果项目中使用了font资源,需要配置mappingFile = file("./resource_mapping.txt"),同时在app目录的resource_mapping.txt文件中添加

1
2
res path mapping:
res/font -> res/font

0x08 一些需要注意的问题

  • 如果不是对APK size有极致的需求,请不要把resources.arsc添加进compressFilePattern.
  • 对于发布于Google Play的APP,建议不要使用7Zip压缩,因为这个会导致Google Play的优化Patch算法失效.
  • compress参数对混淆效果的影响
    若指定compess 参数.png、.gif以及*.jpg,resources.arsc会大大减少安装包体积。若要支持2.2,resources.arsc需保证压缩前小于1M。
  • 操作系统对7z的影响
    实验证明,linux与mac的7z效果更好
  • keepmapping方式对增量包大小的影响
    影响并不大,但使用keepmapping方式有利于保持所有版本混淆的一致性
  • 渠道包的问题(建议通过修改zip摘要的方式生产渠道包)
    在出渠道包的时候,解压重压缩会破坏7zip的效果,通过repackage命令可用7zip重压缩。
  • 通过getIdentifier方式获得资源,需要放置白名单中。
  • 部分手机桌面快捷图标的实现有问题,务必将程序桌面icon加入白名单
  • 第三方SDK的资源加入白名单。可以在white_list.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md查看更多sdk的白名单配置

0x09 相关资源

使用说明:https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md

原理介绍:https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=208135658&idx=1&sn=ac9bd6b4927e9e82f9fa14e396183a8f#rd

white_list.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md

链表(Java版)

链表(Java版)

在链表操作中,我们通常通过生成一个 dummy 哑节点,来避免空链表的判断。

19.删除链表的倒数第 N 个结点

方法一:计算链表长度

为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

方法三:双指针

我们可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

时间复杂度:O(L),其中 L 是链表的长度。
空间复杂度:O(1)。

21.合并两个有序链表

  1. 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里。
  2. 当 l1 和 l2 有一个是空的,只需要简单地将非空链表接在合并链表的后面。

23.合并K个升序链表

方法一:顺序合并

已经有合并两个有序链表的前提,直接遍历数组,依次合并两个链表得到结果。

方法二:分治合并

将 k 个链表分组并将同一组中的链表合并(递归实现)

进时间复杂度为 O(kn×logk)
空间复杂度,递归会使用到 O(logk) 空间代价的栈空间

Android动态权限申请

Android动态权限申请

0x01 介绍

由于 Android 动态权限申请是一个交互比较复杂的模块,整个申请的流程也比较长,所以,写了一个工具来封装了一个,也具体的实现了一个流程。

由于每个App的Ui风格不一致,所以没有把Toast和弹框封装进工具,等后期有好的想法再优化。

0x02 动态权限申请流程

  1. 检查授权状态
  2. 申请权限
  3. 处理权限申请结果
  4. 当用户‘拒绝且不再询问’,引导去手机设置
  5. 检查手机设置后的权限申请结果

0x03 使用说明

主要封装类 PermissionManager

根据业务需要用到安卓定义的高危权限,需要去动态申请权权限。通常是在Activity 和 Fragment 组件中发起。

1 检查权限授权状态

1
2
3
4
5
6
if (PermissionManager.hasPermissions(this, Manifest.permission.READ_PHONE_STATE)) {
// after permission
afterPermission()
} else {
showRequestPermissionDialog("申请手机权限的原因是因为我需要", Manifest.permission.READ_PHONE_STATE)
}

2 申请权限,根据合规化通常需要先弹框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun showRequestPermissionDialog(message: String, vararg perm: String) {
AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage(message)
.setPositiveButton("去授权") { dialog, which ->
dialog.dismiss()
PermissionManager.requestPermissions(this, *perm)
}
.setNegativeButton("取消") { dialog, which ->
dialog.dismiss()
ToastUtil.showToast(this, "已取消授权...")
}
.setCancelable(false)
.create()
.show()
}

3 处理权限申请结果

onRequestPermissionsResult 回调接口中,调用 PermissionManager.onRequestPermissionsResult 方法,并且实现 PermissionManager.OnPermissionResultCallback 这个回调接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
PermissionManager.onRequestPermissionsResult(
this,
requestCode,
permissions,
grantResults,
this
)
}

override fun onPermissionsGranted(requestCode: Int, perms: List<String?>) {
ToastUtil.showToast(this, "授权成功")
afterPermission()
}

override fun onPermissionsDenied(requestCode: Int, perms: List<String?>) {
ToastUtil.showToast(this, "授权失败")
}

override fun onPermissionDeniedForever(requestCode: Int, perms: List<String?>) {
showSettingsDialog()
}

4 当用户‘拒绝且不再询问’,引导去手机设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun showSettingsDialog() {
AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage("已永久拒绝,需要去设置->权限设置打开")
.setPositiveButton("前往设置") { dialog, which ->
dialog.dismiss()
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null))
startActivityForResult(intent, REQUEST_CODE_FOR_PERMISSION_SETTINGS)
}
.setNegativeButton("取消") { dialog, which ->
dialog.dismiss()
ToastUtil.showToast(this, "已取消授权...")
}
.setCancelable(false)
.create()
.show()
}

5 检查手机设置后的权限申请结果

1
2
3
4
5
6
7
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_PERMISSION_SETTINGS) {
afterPermission()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

0x04 特别推荐 PermissionX

PermissionX

中文文档

PermissionX is an extension Android library that makes Android runtime permission request extremely easy. You can use it for basic permission request occasions or handle more complex conditions, like showing rationale dialog or go to app settings for allowance manually.

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×