Merge branch 'support-xml-icons' into 'master'

Support XML icons

Closes #392

See merge request fdroid/fdroidserver!464
This commit is contained in:
Hans-Christoph Steiner 2018-02-19 16:58:28 +00:00
commit 44ebf701e1
3 changed files with 79 additions and 44 deletions

View file

@ -61,7 +61,7 @@ APK_PERMISSION_PAT = \
re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*")
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 = {
"xxxhdpi": '640',
"xxhdpi": '480',
@ -96,9 +96,10 @@ def px_to_dpi(px):
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-%s" % density)
else:
return os.path.join(repodir, "icons-%s" % density)
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))
# extract icons from APK zip file
iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode'])
iconfilename = "%s.%s" % (apk['packageName'], apk['versionCode'])
try:
empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir)
finally:
@ -1442,10 +1443,12 @@ def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
"""
Extracts icons from the given APK zip in various densities,
saves them into given repo directory
and stores their names in the APK metadata dictionary.
"""Extracts PNG icons from an APK with the supported pixel densities
Extracts icons from the given APK zip in various densities, saves
them into given repo directory and stores their names in the APK
metadata dictionary. If the icon is an XML icon, then this tries
to find PNG icon that can replace it.
:param icon_filename: A string representing the icon's file name
:param apk: A populated dictionary containing APK metadata.
@ -1453,7 +1456,17 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
:param apkzip: An opened zipfile.ZipFile of the APK file
:param repo_dir: The directory of the APK's repository
:return: A list of icon densities that are missing
"""
res_name_re = re.compile(r'res/(drawable|mipmap)-(x*[hlm]dpi|anydpi).*/(.*)_[0-9]+dp.(png|xml)')
pngs = dict()
for f in apkzip.namelist():
m = res_name_re.match(f)
if m and m.group(4) == 'png':
density = screen_resolutions[m.group(2)]
pngs[m.group(3) + '/' + density] = m.group(0)
icon_type = None
empty_densities = []
for density in screen_densities:
if density not in apk['icons_src']:
@ -1461,71 +1474,79 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
continue
icon_src = apk['icons_src'][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
if icon_src.endswith('.xml'):
png = os.path.basename(icon_src)[:-4] + '.png'
for f in apkzip.namelist():
if f.endswith(png):
m = re.match(r'res/(drawable|mipmap)-(x*[hlm]dpi).*/', f)
if m and screen_resolutions[m.group(2)] == density:
icon_src = f
m = res_name_re.match(icon_src)
if m:
name = pngs.get(m.group(3) + '/' + str(density))
if name:
icon_src = name
if icon_src.endswith('.xml'):
empty_densities.append(density)
continue
icon_type = '.xml'
icon_dest = os.path.join(icon_dir, icon_filename + icon_type)
try:
with open(icon_dest, 'wb') as f:
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:
logging.warning("Error retrieving icon file: %s %s", icon_dest, e)
del apk['icons_src'][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_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:
f.write(get_icon_bytes(apkzip, icon_src))
im = None
try:
im = Image.open(icon_path)
dpi = px_to_dpi(im.size[0])
for density in screen_densities:
if density in apk['icons']:
break
if density == screen_densities[-1] or dpi >= int(density):
apk['icons'][density] = icon_filename
shutil.move(icon_path,
os.path.join(get_icon_dir(repo_dir, density), icon_filename))
empty_densities.remove(density)
break
except Exception as e:
logging.warning(_("Failed reading {path}: {error}")
.format(path=icon_path, error=e))
finally:
if im and hasattr(im, 'close'):
im.close()
if icon_type == '.png':
im = None
try:
im = Image.open(icon_path)
dpi = px_to_dpi(im.size[0])
for density in screen_densities:
if density in apk['icons']:
break
if density == screen_densities[-1] or dpi >= int(density):
apk['icons'][density] = icon_filename
shutil.move(icon_path,
os.path.join(get_icon_dir(repo_dir, density), icon_filename))
empty_densities.remove(density)
break
except Exception as e:
logging.warning(_("Failed reading {path}: {error}")
.format(path=icon_path, error=e))
finally:
if im and hasattr(im, 'close'):
im.close()
if apk['icons']:
apk['icon'] = icon_filename
apk['icon'] = icon_filename + icon_type
return empty_densities
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 icon_filename: A string representing the icon's file name
:param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key
:param repo_dir: The directory of the APK's repository
"""
icon_filename += '.png'
# First try resizing down to not lose quality
last_density = None
for density in screen_densities:
if density == '65534': # not possible to generate 'anydpi' from other densities
continue
if density not in empty_densities:
last_density = density
continue

Binary file not shown.

View file

@ -253,14 +253,14 @@ class UpdateTest(unittest.TestCase):
apps = fdroidserver.metadata.read_metadata(xref=True)
knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
self.assertEqual(len(apks), 12)
self.assertEqual(len(apks), 13)
apk = apks[0]
self.assertEqual(apk['packageName'], 'com.politedroid')
self.assertEqual(apk['versionCode'], 3)
self.assertEqual(apk['minSdkVersion'], '3')
self.assertEqual(apk['targetSdkVersion'], '3')
self.assertFalse('maxSdkVersion' in apk)
apk = apks[5]
apk = apks[6]
self.assertEqual(apk['packageName'], 'obb.main.oldversion')
self.assertEqual(apk['versionCode'], 1444412523)
self.assertEqual(apk['minSdkVersion'], '4')
@ -298,7 +298,6 @@ class UpdateTest(unittest.TestCase):
raise Exception('This test must be run in the "tests/" subdir')
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',
'120': 'res/drawable-ldpi-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['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):
config = dict()
fdroidserver.common.fill_config_defaults(config)
@ -527,7 +541,7 @@ class UpdateTest(unittest.TestCase):
knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
fdroidserver.update.translate_per_build_anti_features(apps, apks)
self.assertEqual(len(apks), 12)
self.assertEqual(len(apks), 13)
foundtest = False
for apk in apks:
if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: