update: extract and store XML icons

These can then be used by the client.

#344
#392
This commit is contained in:
Hans-Christoph Steiner 2018-02-14 13:44:58 +01:00
parent 5713b54e0b
commit 40fac10ebc
3 changed files with 60 additions and 34 deletions

View file

@ -61,7 +61,7 @@ APK_PERMISSION_PAT = \
re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*") re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*")
APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*") APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*")
screen_densities = ['640', '480', '320', '240', '160', '120'] screen_densities = ['65534', '640', '480', '320', '240', '160', '120']
screen_resolutions = { screen_resolutions = {
"xxxhdpi": '640', "xxxhdpi": '640',
"xxhdpi": '480', "xxhdpi": '480',
@ -96,9 +96,10 @@ def px_to_dpi(px):
def get_icon_dir(repodir, density): def get_icon_dir(repodir, density):
if density == '0': if density == '0' or density == '65534':
return os.path.join(repodir, "icons") return os.path.join(repodir, "icons")
return os.path.join(repodir, "icons-%s" % density) else:
return os.path.join(repodir, "icons-%s" % density)
def get_icon_dirs(repodir): def get_icon_dirs(repodir):
@ -1377,7 +1378,7 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal
.format(apkfilename=apkfile) + str(e)) .format(apkfilename=apkfile) + str(e))
# extract icons from APK zip file # extract icons from APK zip file
iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode']) iconfilename = "%s.%s" % (apk['packageName'], apk['versionCode'])
try: try:
empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir) empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir)
finally: finally:
@ -1464,6 +1465,8 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
if m and m.group(4) == 'png': if m and m.group(4) == 'png':
density = screen_resolutions[m.group(2)] density = screen_resolutions[m.group(2)]
pngs[m.group(3) + '/' + density] = m.group(0) pngs[m.group(3) + '/' + density] = m.group(0)
icon_type = None
empty_densities = [] empty_densities = []
for density in screen_densities: for density in screen_densities:
if density not in apk['icons_src']: if density not in apk['icons_src']:
@ -1471,7 +1474,7 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
continue continue
icon_src = apk['icons_src'][density] icon_src = apk['icons_src'][density]
icon_dir = get_icon_dir(repo_dir, density) icon_dir = get_icon_dir(repo_dir, density)
icon_dest = os.path.join(icon_dir, icon_filename) icon_type = '.png'
# Extract the icon files per density # Extract the icon files per density
if icon_src.endswith('.xml'): if icon_src.endswith('.xml'):
@ -1482,59 +1485,68 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
icon_src = name icon_src = name
if icon_src.endswith('.xml'): if icon_src.endswith('.xml'):
empty_densities.append(density) empty_densities.append(density)
continue icon_type = '.xml'
icon_dest = os.path.join(icon_dir, icon_filename + icon_type)
try: try:
with open(icon_dest, 'wb') as f: with open(icon_dest, 'wb') as f:
f.write(get_icon_bytes(apkzip, icon_src)) f.write(get_icon_bytes(apkzip, icon_src))
apk['icons'][density] = icon_filename apk['icons'][density] = icon_filename + icon_type
except (zipfile.BadZipFile, ValueError, KeyError) as e: except (zipfile.BadZipFile, ValueError, KeyError) as e:
logging.warning("Error retrieving icon file: %s %s", icon_dest, e) logging.warning("Error retrieving icon file: %s %s", icon_dest, e)
del apk['icons_src'][density] del apk['icons_src'][density]
empty_densities.append(density) empty_densities.append(density)
if '-1' in apk['icons_src'] and not apk['icons_src']['-1'].endswith('.xml'): # '-1' here is a remnant of the parsing of aapt output, meaning "no DPI specified"
if '-1' in apk['icons_src']:
icon_src = apk['icons_src']['-1'] icon_src = apk['icons_src']['-1']
icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename) icon_type = icon_src[-4:]
icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename + icon_type)
with open(icon_path, 'wb') as f: with open(icon_path, 'wb') as f:
f.write(get_icon_bytes(apkzip, icon_src)) f.write(get_icon_bytes(apkzip, icon_src))
im = None if icon_type == '.png':
try: im = None
im = Image.open(icon_path) try:
dpi = px_to_dpi(im.size[0]) im = Image.open(icon_path)
for density in screen_densities: dpi = px_to_dpi(im.size[0])
if density in apk['icons']: for density in screen_densities:
break if density in apk['icons']:
if density == screen_densities[-1] or dpi >= int(density): break
apk['icons'][density] = icon_filename if density == screen_densities[-1] or dpi >= int(density):
shutil.move(icon_path, apk['icons'][density] = icon_filename
os.path.join(get_icon_dir(repo_dir, density), icon_filename)) shutil.move(icon_path,
empty_densities.remove(density) os.path.join(get_icon_dir(repo_dir, density), icon_filename))
break empty_densities.remove(density)
except Exception as e: break
logging.warning(_("Failed reading {path}: {error}") except Exception as e:
.format(path=icon_path, error=e)) logging.warning(_("Failed reading {path}: {error}")
finally: .format(path=icon_path, error=e))
if im and hasattr(im, 'close'): finally:
im.close() if im and hasattr(im, 'close'):
im.close()
if apk['icons']: if apk['icons']:
apk['icon'] = icon_filename apk['icon'] = icon_filename + icon_type
return empty_densities return empty_densities
def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir): def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir):
""" """
Resize existing icons for densities missing in the APK to ensure all densities are available Resize existing PNG icons for densities missing in the APK to ensure all densities are available
:param empty_densities: A list of icon densities that are missing :param empty_densities: A list of icon densities that are missing
:param icon_filename: A string representing the icon's file name :param icon_filename: A string representing the icon's file name
:param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key :param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key
:param repo_dir: The directory of the APK's repository :param repo_dir: The directory of the APK's repository
""" """
icon_filename += '.png'
# First try resizing down to not lose quality # First try resizing down to not lose quality
last_density = None last_density = None
for density in screen_densities: for density in screen_densities:
if density == '65534': # not possible to generate 'anydpi' from other densities
continue
if density not in empty_densities: if density not in empty_densities:
last_density = density last_density = density
continue continue

Binary file not shown.

View file

@ -253,14 +253,14 @@ class UpdateTest(unittest.TestCase):
apps = fdroidserver.metadata.read_metadata(xref=True) apps = fdroidserver.metadata.read_metadata(xref=True)
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
self.assertEqual(len(apks), 12) self.assertEqual(len(apks), 13)
apk = apks[0] apk = apks[0]
self.assertEqual(apk['packageName'], 'com.politedroid') self.assertEqual(apk['packageName'], 'com.politedroid')
self.assertEqual(apk['versionCode'], 3) self.assertEqual(apk['versionCode'], 3)
self.assertEqual(apk['minSdkVersion'], '3') self.assertEqual(apk['minSdkVersion'], '3')
self.assertEqual(apk['targetSdkVersion'], '3') self.assertEqual(apk['targetSdkVersion'], '3')
self.assertFalse('maxSdkVersion' in apk) self.assertFalse('maxSdkVersion' in apk)
apk = apks[5] apk = apks[6]
self.assertEqual(apk['packageName'], 'obb.main.oldversion') self.assertEqual(apk['packageName'], 'obb.main.oldversion')
self.assertEqual(apk['versionCode'], 1444412523) self.assertEqual(apk['versionCode'], 1444412523)
self.assertEqual(apk['minSdkVersion'], '4') self.assertEqual(apk['minSdkVersion'], '4')
@ -298,7 +298,6 @@ class UpdateTest(unittest.TestCase):
raise Exception('This test must be run in the "tests/" subdir') raise Exception('This test must be run in the "tests/" subdir')
apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk') apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk')
self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png', self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png',
'120': 'res/drawable-ldpi-v4/icon_launcher.png', '120': 'res/drawable-ldpi-v4/icon_launcher.png',
'160': 'res/drawable-mdpi-v4/icon_launcher.png', '160': 'res/drawable-mdpi-v4/icon_launcher.png',
@ -319,6 +318,21 @@ class UpdateTest(unittest.TestCase):
self.assertEqual(apk_info['hashType'], 'sha256') self.assertEqual(apk_info['hashType'], 'sha256')
self.assertEqual(apk_info['targetSdkVersion'], '8') self.assertEqual(apk_info['targetSdkVersion'], '8')
apk_info = fdroidserver.update.scan_apk('org.bitbucket.tickytacky.mirrormirror_4.apk')
self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable-mdpi/mirror.png',
'-1': 'res/drawable-mdpi/mirror.png'})
apk_info = fdroidserver.update.scan_apk('repo/info.zwanenburg.caffeinetile_4.apk')
self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable/ic_coffee_on.xml',
'-1': 'res/drawable/ic_coffee_on.xml'})
apk_info = fdroidserver.update.scan_apk('repo/com.politedroid_6.apk')
self.assertEqual(apk_info['icons_src'], {'120': 'res/drawable-ldpi-v4/icon.png',
'160': 'res/drawable-mdpi-v4/icon.png',
'240': 'res/drawable-hdpi-v4/icon.png',
'320': 'res/drawable-xhdpi-v4/icon.png',
'-1': 'res/drawable-mdpi-v4/icon.png'})
def test_scan_apk_no_sig(self): def test_scan_apk_no_sig(self):
config = dict() config = dict()
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
@ -527,7 +541,7 @@ class UpdateTest(unittest.TestCase):
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
fdroidserver.update.translate_per_build_anti_features(apps, apks) fdroidserver.update.translate_per_build_anti_features(apps, apks)
self.assertEqual(len(apks), 12) self.assertEqual(len(apks), 13)
foundtest = False foundtest = False
for apk in apks: for apk in apks:
if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: