oynix

于无声处听惊雷,于无色处见繁花

Android工具类产品方法汇总

工具类产品一般泛指那几大类功能,包括但不限于垃圾清理、手机加速、文件管理、应用管理,等等。

1. 获取存储总空间、可用空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun getSDTotalSize(): Long {
val path = Environment.getExternalStorageDirectory()
val stat = StatFs(path.path)
val blockSize = stat.blockSizeLong
val totalBlocks = stat.blockCountLong
return blockSize * totalBlocks
}

fun getSDAvailableSize(): Long {
val path = Environment.getExternalStorageDirectory()
val stat = StatFs(path.path)
val blockSize = stat.blockSizeLong
val availableBlocks = stat.availableBlocksLong
return blockSize * availableBlocks
}

2. 获取总RAM、可用RAM

1
2
3
4
5
6
7
8
9
10
11
12
13
fun getTotalMemorySize(ctx: Context): Long {
val mActivityManager = ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
mActivityManager.getMemoryInfo(memoryInfo)
return memoryInfo.totalMem
}

fun getAvailableMemorySize(ctx: Context): Long {
val mActivityManager = ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
mActivityManager.getMemoryInfo(memoryInfo)
return memoryInfo.availMem
}

3. 垃圾文件

从访问权限这个维度来看的话,手机内的目录可以分成三个大类,应用内私有、应用内共有和共有目录。应用内的目录不管是私有还有共有,都是不需要权限的,而共有目录则需要READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限才可以访问和操作。

应用内私有的目录在/data/data/文件夹下面,以应用包名命名文件夹,每个文件夹对应一个应用,每个应用只可以访问自己的文件夹,且不需要权限。

应用内共有的目录在/sdcard/Android/data/文件夹下面,同样也是以包名命名文件夹,每个文件夹对应一个应用,访问自己包名文件夹时不需要权限。

共有目录对应的是/sdcard/文件夹,申请权限时申请的便是这个目录的读写权限,用户同意授权之后便可以拥有这个目录下所有文件和文件夹的操作权限,包括上面的/sdcard/Android/data/文件夹。

手机设备中一般都有个文件管理的应用,这个应用展示的多是/sdcard/目录,所以,用户也可以看到应用内公开的/sdcard/Android/data目录下的内容。

扫描垃圾文件时,一般指的是/sdcard/Android/data/{包名}/cache文件夹下的内容,顾名思义,这个文件夹下是应用运行过程中产生的缓存文件,姑且认定缓存文件等同于垃圾文件,可以删除。要注意,并不是所有的应用都有这个cache目录,产生了缓存才会创建这个文件夹。所以,扫描垃圾文件时,先要申请/sdcard/的读写权限,然后遍历/sdcard/Android/data/下的每个文件夹即可。

因为文件夹名称就是包名,所以很容易就可获取对应的icon,应用名称,并且计算其缓存文件的大小。当然,会有例外,有些文件夹的名称便不是包名,可能对应一个系统应用,可忽略不计。

1
2
3
4
5
val info = packageManager.getApplicationInfo(packageName, 0)
// icon
info.loadIcon(packageManager)
// application name
info.loadLabel(packageManager)

以上,是常规化的文件路径,既然有常规的,那就有不按套路出牌的。

有些应用在获取到共有目录/sdcard/的读写权限后,会在该目录下创建文件夹存储自己的一些文件,有些文件可能有用,有些是无用的可清理文件。这个时候光凭名字就不能断定了,有些第三方提供了数据库,里面总结了市面上常用的一些应用固定的存储路径习惯,包括哪些有用、哪些可删除,使用这些数据库,可以多扫描出一些垃圾文件,提升清理效果。

注意
在Android 10(target 29)及更高的版本,采用了新的存储范例,叫做分区存储。相比于旧的存储模型,进一步收紧了权限。在新的存储模型中,应用只可以访问自己私有的目录,包括内部存储的私有目录()和外部存储的私有目录(),以及一些固定的媒体目录,如果图片目录、视频目录、下载文件目录,等等。除此之外,其他路径均不可访问。

在Android 10上,可通过这种方式,来继续使用旧存储模型,

1
2
<application
android:requestLegacyExternalStorage="true" />

在Android 11上,可通过这种方式,让覆盖安装的应用继续使用旧存储模型,如果是新安装,则没有解决方案。这里的覆盖安装是指,已安装的应用target小于30,新版本的应用target为30,如果设置了preserve,则可以继续使用旧存粗模型,如果重装或新装target为30的安装包,那么就不可以了。

1
2
<application
android:preserveLegacyExternalStorage="true" />

4. 通知管理

这里的通知管理,指的是管理通知栏里的通知,常规操作就是把用户选择的不想看到的应用所发的通知给它隐藏起来。一般是两步走,第一步是获取管理通知的权限,拿到权限后,就可以监听系统发送通知,在收到通知时作出处理。

判断是否已经拿到权限。

这里使用的方式是,先拿到所有已经获取管理通知权限的组件,然后判断这里面有没有自己。拿到数据格式一般是这样的,

1
com.miui.powerkeeper/com.miui.powerkeeper.NotificationListener:com.miui.powerkeeper/com.miui.powerkeeper.NotificationListener

也就是包名/组件名:包名/组件名,多个应用间用冒号分离,所以判断方式如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
fun checkNotificationListenerEnabled(ctx: Context): Boolean {
val components = Settings.Secure.getString(ctx.contentResolver, "enabled_notification_listeners")
if (!TextUtils.isEmpty(components)) {
val names = components.split(":").toTypedArray()
for (i in names.indices) {
val cn = ComponentName.unflattenFromString(names[i])
if (cn != null && TextUtils.equals(ctx.packageName, cn.packageName)) {
return true
}
}
}
return false
}

如果还没有权限,要引导用户跳转到系统设置中,给自己授权,

1
2
3
val intent = Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)

直接跳过去之后,里面展示的是手机里所有申请管理通知权限的应用,以及用户是否授权,但是,你会发现里面并没有自己,这是因为还没有申请这个权限,所以在授权页面自然也不会看见。申请的具体方式是,创建一个类,继承自NotificationListenerService,并注册到AndroidManifest.xml中,同时声明这个Service需要的权限

1
2
class NotiService : NotificationListenerService() {
}
1
2
3
4
5
6
7
<service android:name=".NotiService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

NotificationListenerService中,有个onListenerConnected方法,当启动时具有权限,或者用户授予权限时,系统就会回调这个方法,在此处可以判断用户是否已授权。此外,还有个onNotificationPosted方法,当处于授权状态时,一旦系统收到通知,这个方法会立刻被回调,所以在这个方法里可以操作通知,比如通过cancl方法取消通知,以此来避免对用户的打扰。

1
2
3
4
5
6
7
8
9
10
class NotiService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
cancelNotification(sbn?.key)
}

override fun onListenerConnected() {
super.onListenerConnected()
}
}

5. 应用锁

顾名思义,这个功能可以给手机上的应用加上一个锁,防止进入。具体的实现方式是,实时监听手机的状态,数据类似adb shell dumpsys activity命令的输出,判断当前运行、或者刚刚运行的应用是否在用户需要锁定的应用范围之内,如果在,则在这个应用上覆盖一层View,用来拦截后续操作

这个过程中,涉及到两个权限,一个是查看手机应用使用情况的权限,有了这个权限才可以知道应用启动事件,另个是悬浮窗口权限,有了这个权限才可以在其他应用上面盖上一层View进行阻拦。

  • 应用使用情况权限
    首先需要在AndroidManifest.xml文件中声明,然后在合适的时机判断是否已获取,如果没有获取则需要打开系统设置页面,引导用户授权。

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

    判断是否已获取,

    1
    2
    3
    4
    5
    fun checkUsageStatsPermission(ctx: Context): Boolean {
    val aom = ctx.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
    val mode = aom.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), ctx.packageName)
    return mode == AppOpsManager.MODE_ALLOWED
    }

    跳转至系统设置页面,引导用户授权,

    1
    2
    3
    4
    fun openUsageStatsSetting(ctx: Context) {
    val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS, Uri.parse("package:" + ctx.packageName))
    activity.startActivity(intent)
    }
  • 上层显示权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fun checkCanDrawOverlays(ctx: Context): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    return Settings.canDrawOverlays(ctx)
    }
    return true
    }

    fun openDrawOverlaysSetting(ctx: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + ctx.packageName))
    ctx.startActivity(ctx)
    }
    }

注意,这个权限不是必须的,具体要看处理需求,因为可以感知到其他应用启动,所以在监测到启动后,直接可以采取操作,比如打开一个验证密码的叶脉内,而不是非要在其上面覆盖一层View。

至于如何获取当前正在运行的应用,也就是任务栈栈顶的应用,如果是API 21以下,使用ActivityManager,getRunningTask即可,如果高于这个版本,则需要用到刚刚获取的那个权限,应用使用情况权限。

通过这个权限可以获取到手机一段时间内触发的所有事件,事件的类型有很多,比如ACTIVITY_PAUSED,ACTIVITY_RESUMED等等,具体可以查看
anddroid.app.usage.UsageEvents.Event

具体做法就是,每隔一段时间就就查询一下ACTIVITY_RESUMED事件,因为只要是应用启动,一定会触发这个事件,然后通过这个事件的包名来判断,这个包名是否是用户设置了应用锁的应用,如果是,则进行拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun getResumedPackageName(ctx: Context): String {
val usm = ctx.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val now = System.currentTimeMillis()
val events = usm.queryEvents(now - 10 * 1000, now)
val event: UsageEvents.Evnet? = null
while (events.hasNextEvent()) {
val e = UsageEvents.Event()
events.getNextEvent(e)
if (e.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
event = e
}
}

return event?.packageName ?: ""
}

这种方式有个致命的弊端,那就是必须保证自己存活,如果把检测操作放在一个单独的Service里,那就就需要保证Service不死才可以。至于如何保活,那就是另个沉重的话题了,暂且按下不表。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/08/9572fa288aa3/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道