1、申 请 I D:gfdgd xi
2、个人邮箱:3025613752@qq.com
3、原创技术文章:对于 UEngine 运行器的代码分析(现写,全网唯一)
项目链接:https://gitee.com/gfdgd-xi/uengine-runner,代码均按照 GPLV3 协议开源
新版本Deepin/UOS发布后,可以在应用商店安装部分官方已适配的安卓应用,对爱好者来说,不能自己安装APK软件包始终差点意思,本程序可以为Deepin/UOS上的UEngine安卓运行环境安装自定义APK软件包,并能发送安装的APK包启动菜单到桌面或系统菜单。
而这个程序核心的难点是关于 APK 信息的获取,本人也并没有找到一个比较适合的轮子
(如果不想从主文件 mainwindow.py 看代码,也提供了相应的 API:https://gitee.com/gfdgd-xi/uengine-runner/tree/main/api)
除了对 UEngine(deepin/UOS 出品的一款 Android 模拟器)的操作,其它大多数还是通用的
获取 APK 信息主要是调用了 aapt,可以通过,从以下源代码即可看出:
[Python] 纯文本查看 复制代码
# 获取 aapt 的所有信息
def GetApkInformation(apkFilePath: "apk 所在路径")->"获取 aapt 的所有信息":
return GetCommandReturn("aapt dump badging '{}'".format(apkFilePath))
是调用了 [Asm] 纯文本查看 复制代码 aapt dump badging apk文件 的命令,返回的为如下内容(接下来均以程序 QQ 为例)
[Bash shell] 纯文本查看 复制代码
package: name='com.tencent.mobileqq' versionCode='3118' versionName='8.9.3' platformBuildVersionName=''
install-location:'auto'
sdkVersion:'21'
targetSdkVersion:'26'
uses-permission: name='com.android.launcher.permission.INSTALL_SHORTCUT'
uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.VIBRATE'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.CHANGE_CONFIGURATION'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='android.permission.SYSTEM_ALERT_WINDOW'
uses-permission: name='android.permission.RECORD_AUDIO'
uses-permission: name='com.tencent.msf.permission.account.sync'
uses-permission: name='android.permission.MODIFY_AUDIO_SETTINGS'
uses-permission: name='android.permission.CAMERA'
uses-permission: name='android.permission.CHANGE_WIFI_STATE'
uses-permission: name='android.permission.ACCESS_WIFI_STATE'
uses-permission: name='android.permission.KILL_BACKGROUND_PROCESSES'
uses-permission: name='com.android.launcher.permission.READ_SETTINGS'
uses-permission: name='com.android.launcher.permission.UNINSTALL_SHORTCUT'
uses-permission: name='android.permission.PERSISTENT_ACTIVITY'
uses-permission: name='android.permission.WRITE_SETTINGS'
uses-permission: name='android.permission.GET_TASKS'
uses-permission: name='com.tencent.permission.VIRUS_SCAN'
uses-permission: name='android.permission.READ_LOGS'
uses-permission: name='android.permission.READ_CONTACTS'
uses-permission: name='android.permission.FLASHLIGHT'
uses-permission: name='android.permission.BLUETOOTH'
uses-permission: name='android.permission.BLUETOOTH_ADMIN'
uses-permission: name='android.permission.BROADCAST_STICKY'
uses-permission: name='android.permission.WRITE_CONTACTS'
uses-permission: name='android.permission.WRITE_OWNER_DATA'
uses-permission: name='android.permission.SYSTEM_OVERLAY_WINDOW'
uses-permission: name='android.permission.USE_FINGERPRINT'
uses-permission: name='com.soter.permission.ACCESS_SOTER_KEYSTORE'
uses-permission: name='android.permission.USE_FACERECOGNITION'
uses-permission: name='android.permission.CHANGE_NETWORK_STATE'
uses-permission: name='android.permission.EXPAND_STATUS_BAR'
uses-permission: name='android.permission.INTERACT_ACROSS_USERS'
uses-permission: name='com.android.launcher.permission.WRITE_SETTINGS'
uses-permission: name='com.android.launcher2.permission.READ_SETTINGS'
uses-permission: name='com.android.launcher3.permission.READ_SETTINGS'
uses-permission: name='com.android.launcher3.permission.WRITE_SETTINGS'
uses-permission: name='com.google.android.launcher.permission.READ_SETTINGS'
uses-permission: name='com.bbk.launcher2.permission.READ_SETTINGS'
uses-permission: name='com.huaqin.launcherEx.permission.READ_SETTINGS'
uses-permission: name='com.htc.launcher.settings'
uses-permission: name='com.htc.launcher.permission.READ_SETTINGS'
uses-permission: name='com.htc.launcher.permission.WRITE_SETTINGS'
uses-permission: name='com.huawei.launcher2.permission.READ_SETTINGS'
uses-permission: name='com.huawei.launcher3.permission.READ_SETTINGS'
uses-permission: name='com.huawei.android.launcher.permission.READ_SETTINGS'
uses-permission: name='com.oppo.launcher.permission.READ_SETTINGS'
uses-permission: name='com.android.launcher2.permission.READ_SETTINGS'
uses-permission: name='com.meizu.android.launcher.permission.READ_SETTINGS'
uses-permission: name='com.meizu.launcher2.permission.READ_SETTINGS'
uses-permission: name='com.lenovo.launcher.permission.READ_SETTINGS'
uses-permission: name='com.ebproductions.android.launcher.permission.READ_SETTINGS'
uses-permission: name='com.sec.android.app.twlauncher.settings.READ_SETTINGS'
uses-permission: name='com.fede.launcher.permission.READ_SETTINGS'
uses-permission: name='net.qihoo.launcher.permission.READ_SETTINGS'
uses-permission: name='com.qihoo360.launcher.permission.READ_SETTINGS'
uses-permission: name='com.lge.launcher.permission.READ_SETTINGS'
uses-permission: name='org.adw.launcher.permission.READ_SETTINGS'
uses-permission: name='telecom.mdesk.permission.READ_SETTINGS'
uses-permission: name='com.tencent.mobileqq.permission.Pandora'
uses-permission: name='com.sonyericsson.home.permission.BROADCAST_BADGE'
uses-permission: name='com.sec.android.provider.badge.permission.READ'
uses-permission: name='com.sec.android.provider.badge.permission.WRITE'
uses-permission: name='com.lenovo.launcher.permission.BADGE_READ'
uses-permission: name='com.lenovo.launcher.permission.BADGE_WRITE'
uses-permission: name='com.huawei.android.launcher.permission.CHANGE_BADGE'
uses-permission: name='com.huawei.authentication.HW_ACCESS_AUTH_SERVICE'
uses-permission: name='android.permission.GET_ACCOUNTS'
uses-permission: name='android.permission.MANAGE_ACCOUNTS'
uses-permission: name='android.permission.AUTHENTICATE_ACCOUNTS'
uses-permission: name='android.permission.WRITE_CONTACTS'
uses-permission: name='android.permission.READ_SYNC_SETTINGS'
uses-permission: name='android.permission.WRITE_SYNC_SETTINGS'
uses-permission: name='android.permission.DISABLE_KEYGUARD'
uses-permission: name='android.permission.CHANGE_WIFI_MULTICAST_STATE'
uses-permission: name='com.tencent.mobileqq.permission.MM_MESSAGE'
uses-permission: name='android.permission.WRITE_CALENDAR'
uses-permission: name='android.permission.READ_CALENDAR'
uses-permission: name='android.permission.RESTART_PACKAGES'
uses-permission: name='android.permission.NFC'
uses-permission: name='android.permission.READ_APP_BADGE'
uses-permission: name='android.permission.READ_PACKAGE_BADGE'
uses-permission: name='miui.permission.READ_STEPS'
uses-permission: name='android.permission.REQUEST_INSTALL_PACKAGES'
uses-permission: name='com.google.android.gms.permission.ACTIVITY_RECOGNITION'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
uses-permission: name='com.tencent.mobileqq.permission.TMF_SHARK'
uses-permission: name='com.tencent.photos.permission.DATA'
uses-permission: name='com.tencent.msf.permission.account.sync'
uses-permission: name='com.tencent.music.data.permission2'
uses-permission: name='android.permission.CHANGE_WIFI_STATE'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_WIFI_STATE'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.ACCESS_LOCATION'
uses-permission: name='android.permission.ACCESS_FINE_LOCATION'
uses-permission: name='android.permission.ACCESS_COARSE_LOCATION'
uses-permission: name='android.permission.CAMERA'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='com.android.launcher.permission.INSTALL_SHORTCUT'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='com.tencent.msg.permission.pushnotify'
uses-permission: name='com.tencent.msf.permission.account.sync'
uses-permission: name='com.tencent.wifisdk.permission.disconnect'
uses-permission: name='com.tencent.qqhead.permission.getheadresp'
uses-permission: name='com.tencent.qzone.permission.notify'
uses-permission: name='com.tencent.mobileqq.permission.MIPUSH_RECEIVE'
uses-permission: name='com.tencent.qav.permission.broadcast'
uses-permission: name='android.permission.ALARM_LOCK'
uses-permission: name='android.permission.REORDER_TASKS'
uses-permission: name='com.tencent.mobileqq.vfs.broadcast'
uses-permission: name='com.tencent.mobileqq.backtrace.warmed_up'
uses-permission: name='com.android.vending.CHECK_LICENSE'
uses-permission: name='com.tencent.mobileqq.permission.PROCESS_PUSH_MSG'
uses-permission: name='com.tencent.mobileqq.permission.PUSH_PROVIDER'
uses-permission: name='com.coloros.mcs.permission.RECIEVE_MCS_MESSAGE'
uses-permission: name='com.vivo.assistant.StepProvider'
uses-permission: name='com.vivo.assistant.permission.access.provider'
uses-permission: name='com.vivo.assistant.permission.sport.broadcast'
uses-permission: name='com.asus.msa.SupplementaryDID.ACCESS'
uses-permission: name='com.oplus.ocs.permission.third'
application-label:'QQ'
application-icon-120:'r/b/icon.png'
application-icon-160:'r/b/icon.png'
application-icon-213:'r/b/icon.png'
application-icon-240:'r/b/icon.png'
application-icon-320:'r/b/icon.png'
application-icon-480:'r/b/icon.png'
application-icon-640:'r/b/icon.png'
application-icon-65534:'r/b/icon.png'
application-icon-65535:'r/b/icon.png'
application: label='QQ' icon='r/b/icon.png'
uses-library-not-required:'com.sec.android.app.multiwindow'
uses-library-not-required:'scamera_sep'
launchable-activity: name='com.tencent.mobileqq.activity.SplashActivity' label='QQ' icon=''
uses-library-not-required:'com.google.android.media.effects'
uses-library-not-required:'com.motorola.hardware.frontcamera'
uses-library-not-required:'org.apache.http.legacy'
uses-library-not-required:'soterkeystore'
uses-permission: name='android.permission.READ_EXTERNAL_STORAGE'
uses-implied-permission: name='android.permission.READ_EXTERNAL_STORAGE' reason='requested WRITE_EXTERNAL_STORAGE'
feature-group: label=''
uses-gl-es: '0x20000'
uses-feature-not-required: name='android.hardware.bluetooth_le'
uses-feature: name='android.hardware.camera'
uses-feature-not-required: name='android.hardware.camera.autofocus'
uses-feature-not-required: name='android.hardware.location'
uses-feature-not-required: name='android.hardware.location.gps'
uses-feature-not-required: name='android.hardware.location.network'
uses-feature-not-required: name='android.hardware.telephony'
uses-feature: name='android.hardware.bluetooth'
uses-implied-feature: name='android.hardware.bluetooth' reason='requested android.permission.BLUETOOTH permission, requested android.permission.BLUETOOTH_ADMIN permission, and targetSdkVersion > 4'
uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
uses-feature: name='android.hardware.microphone'
uses-implied-feature: name='android.hardware.microphone' reason='requested android.permission.RECORD_AUDIO permission'
uses-feature: name='android.hardware.screen.landscape'
uses-implied-feature: name='android.hardware.screen.landscape' reason='one or more activities have specified a landscape orientation'
uses-feature: name='android.hardware.screen.portrait'
uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation'
uses-feature: name='android.hardware.wifi'
uses-implied-feature: name='android.hardware.wifi' reason='requested android.permission.ACCESS_WIFI_STATE permission, requested android.permission.CHANGE_WIFI_MULTICAST_STATE permission, and requested android.permission.CHANGE_WIFI_STATE permission'
provides-component:'wallpaper'
provides-component:'payment'
main
other-activities
other-receivers
other-services
supports-screens: 'small' 'normal' 'large' 'xlarge'
supports-any-density: 'true'
locales: '--_--'
densities: '120' '160' '213' '240' '320' '480' '640' '65534' '65535'
native-code: 'arm64-v8a'
从以上内容可以看到程序申请的权限、图标在包里的路径、程序名、版本、包名、activity等等,可以获取大多数的信息,因此,就可以基于 aapt 获取 APK 信息
首先是包名和版本号,从获取信息的第一行就可以看到了
[Asm] 纯文本查看 复制代码 package: name='com.tencent.mobileqq' versionCode='3118' versionName='8.9.3' platformBuildVersionName=''
因此采用遍历读取每一行开头是否含有,定位到所在行,包名排除掉 versionCode 后面和 name 及其前面即可获取包名
版本号排除掉 versionName 及其前面和 platformBuildVersionName 即可
写出的代码如下:
[Python] 纯文本查看 复制代码
# 获取 apk 包名
def GetApkPackageName(apkFilePath: "apk 所在路径")->"获取 apk 包名":
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "package:" in line:
line = line[0: line.index("versionCode='")]
line = line.replace("package:", "")
line = line.replace("name=", "")
line = line.replace("'", "")
line = line.replace(" ", "")
return line
[Python] 纯文本查看 复制代码
# 获取 APK 版本号
def GetApkVersion(apkFilePath):
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "package:" in line:
if "compileSdkVersion='" in line:
line = line.replace(line[line.index("compileSdkVersion='"): -1], "")
if "platform" in line:
line = line.replace(line[line.index("platform"): -1], "")
line = line.replace(line[0: line.index("versionName='")], "")
line = line.replace("versionName='", "")
line = line.replace("'", "")
line = line.replace(" ", "")
return line
而打开 Android 应用不是只有包名就可以打开的,还需要程序的activity,同样还是使用 aapt 返回的信息,在 143 行
[Asm] 纯文本查看 复制代码 launchable-activity: name='com.tencent.mobileqq.activity.SplashActivity' label='QQ' icon=''
截取中间的 com.tencent.mobileqq.activity.SplashActivity 即可,实现的代码如下:
[Python] 纯文本查看 复制代码 # 获取 apk Activity
def GetApkActivityName(apkFilePath: "apk 所在路径")->"获取 apk Activity":
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "launchable-activity" in line:
line = line[0: line.index("label='")]
line = line.replace("launchable-activity: ", "")
line = line.replace("'", "")
line = line.replace(" ", "")
line = line.replace("name=", "")
line = line.replace("label=", "")
line = line.replace("icon=", "")
return line
而获取程序名称也和上面一样了,信息位于第 140 行,截取label=''中间的内容即可
[Asm] 纯文本查看 复制代码 application: label='QQ' icon='r/b/icon.png'
[Python] 纯文本查看 复制代码 # 获取软件的中文名称
def GetApkChineseLabel(apkFilePath)->"获取软件的中文名称":
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "application-label:" in line:
line = line.replace("application-label:", "")
line = line.replace("'", "")
return line
而获取图标是全部信息获取难度最高的,首先从上面的示例文件中可以看到第 140 行就指定了图标在 APK 内的路径,而 APK 本质也是一个压缩包,因此我们拿 zipfile 这个库进行解压
[Asm] 纯文本查看 复制代码 application: label='QQ' icon='r/b/icon.png'
下面是 UEngine 运行器 1.2.3 (commit:fde7a0d,文件名:main.py)实现读取图标的代码(1.3.0 就更新了获取方式了)
[Python] 纯文本查看 复制代码
# 获取图标在包内的路径
def GetApkIconInApk(apkFilePath)->"获取图标在包内的路径":
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "application:" in line:
line = line[line.index("icon='"): -1]
line = line.replace("icon='", "")
if "'" in line:
line = line[0: line.index("'")]
return line
# 获取 apk 文件的图标(部分程序不支持)
def SaveApkIcon(apkFilePath, iconSavePath)->"获取 apk 文件的图标(部分程序不支持)":
zip = zipfile.ZipFile(apkFilePath)
iconData = zip.read(GetApkIconInApk(apkFilePath))
with open(iconSavePath, 'w+b') as saveIconFile:
saveIconFile.write(iconData)
但是如果搞像 B 站这个应用,会发现提取不了图标,你会巧妙的发现,指向的不是 png 而是 xml
[Asm] 纯文本查看 复制代码 application: label='哔哩哔哩' icon='res/uFR.xml'
这是目前 1.8.0 的获取代码(因为这一块并不是我贡献的,所以不详细介绍)
(核心)getxmlimg.py:
[Python] 纯文本查看 复制代码
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import zipfile
import subprocess
import re
class getsavexml():
def savexml(self,apkFilePath,xmlpath,iconSavePath):
cmddumpid = "aapt dump xmltree "+ apkFilePath + " " + xmlpath
print(cmddumpid)
xmltree = subprocess.getoutput(cmddumpid)
xmls = xmltree.splitlines()
# find strs ,print next line
def FindStrs(lines,strs):
i=0
while i < len(lines):
if re.search(strs,lines[i]):
tmpstr = lines[i+1]
i += 1
Resultstr = tmpstr.split(":")[-1].split("=")[-1].split("0x")[-1]
return Resultstr
else:
i += 1
#从apk的信息中获取前后景图片的ID号
backimgid = FindStrs(xmls,"background")
foreimgid = FindStrs(xmls,"foreground")
print(backimgid)
print(foreimgid)
# 直接从apk resource文件获取前后两层图片路径及ID字符串
resource = subprocess.getoutput("aapt dump --values resources " + apkFilePath + "| grep -iE -A1 " + "\"" + backimgid + "|" + foreimgid + "\"")
resourcelines = resource.splitlines()
print(resourcelines)
# 从过滤出的字符串中获取所有相同ID的图片路径
def Findpicpath(lines,imgid):
i=0
Resultstr = []
while i < len(lines):
if re.search(imgid,lines[i]) and re.search("string8",lines[i+1]) :
print(lines[i+1])
tmpstr = lines[i+1].replace("\"","")
i += 1
Resultstr.append(tmpstr.split()[-1])
else:
i += 1
return Resultstr
#获取所有带前后图片ID的图片路径(相同背景或者前景的图片ID但分辨率不一样)
backimgs = Findpicpath(resourcelines,backimgid)
foreimgs = Findpicpath(resourcelines,foreimgid)
print(backimgs)
print(foreimgs)
#获取分辨率最高的图片路径
def getmaxsize(imgs):
j = 0
size=(0,0)
zipapk = zipfile.ZipFile(apkFilePath)
imgpath = ""
while j < len(imgs):
print(imgs[j])
img = Image.open(zipapk.open(imgs[j]))
print(imgs[j])
print(img.size)
if size < img.size:
size = img.size
imgpath = imgs[j]
j += 1
return imgpath
# 获取到文件列表后,进行比较分辨率,选取分辨率最高的张图片
iconbackpath = getmaxsize(backimgs)
iconforepath = getmaxsize(foreimgs)
print(iconbackpath + " " + iconforepath)
#从APK文件获取最终图片
zipapk = zipfile.ZipFile(apkFilePath)
iconback = zipapk.open(iconbackpath)
iconfore = zipapk.open(iconforepath)
# 叠加图片,mask 设置前景为蒙版
iconbackimg = Image.open(iconback).convert("RGBA")
iconforeimg = Image.open(iconfore).convert("RGBA")
iconbackimg.paste(iconforeimg,mask=iconforeimg)
# 圆角图片函数,网上拷贝的
def circle_corner(img, radii): #把原图片变成圆角,这个函数是从网上找的,原址 https://www.pyget.cn/p/185266
"""
圆角处理
:param img: 源图象。
:param radii: 半径,如:30。
:return: 返回一个圆角处理后的图象。
"""
# 画圆(用于分离4个角)
circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建一个黑色背景的画布
draw = ImageDraw.Draw(circle)
draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 画白色圆形
# 原图
img = img.convert("RGBA")
w, h = img.size
# 画4个角(将整圆分离为4个部分)
alpha = Image.new('L', img.size, 255)
alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角
alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) # 右上角
alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii)) # 右下角
alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) # 左下角
# alpha.show()
img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见
return img
# 圆角半径1/8边长,保存icon图片
w,h = iconbackimg.size
iconimg = circle_corner(iconbackimg,int(w/8))
iconimg.save(iconSavePath)
mainwindow.py:
[Python] 纯文本查看 复制代码
# 保存apk图标
def SaveApkIcon(apkFilePath, iconSavePath)->"保存 apk 文件的图标":
try:
if os.path.exists(iconSavePath):
os.remove(iconSavePath)
info = GetApkInformation(apkFilePath)
for line in info.split('\n'):
if "application:" in line:
xmlpath = line.split(":")[-1].split()[-1].split("=")[-1].replace("'","")
if xmlpath.endswith('.xml'):
xmlsave = getsavexml()
print(xmlpath)
xmlsave.savexml(apkFilePath,xmlpath,iconSavePath)
return
else:
zip = zipfile.ZipFile(apkFilePath)
iconData = zip.read(xmlpath)
with open(iconSavePath, 'w+b') as saveIconFile:
saveIconFile.write(iconData)
return
print("None Icon! Show defult icon")
shutil.copy(programPath + "/defult.png", iconSavePath)
except:
traceback.print_exc()
print("Error, show defult icon")
shutil.copy(programPath + "/defult.png", iconSavePath)
其它的例如 root 镜像、键盘映射、添加 adb 运行列表就不详细介绍了
核心的功能就完成了
(到时候审核过/没过后再传代码仓库和写 README) |