return repo_files, cachechanged
+def scan_apk(apk_file):
+ """
+ Scans an APK file and returns dictionary with metadata of the APK.
+
+ Attention: This does *not* verify that the APK signature is correct.
+
+ :param apk_file: The (ideally absolute) path to the APK file
+ :raises BuildException
+ :return A dict containing APK metadata
+ """
+ apk = {
+ 'hash': sha256sum(apk_file),
+ 'hashType': 'sha256',
+ 'uses-permission': [],
+ 'uses-permission-sdk-23': [],
+ 'features': [],
+ 'icons_src': {},
+ 'icons': {},
+ 'antiFeatures': set(),
+ }
+
+ if SdkToolsPopen(['aapt', 'version'], output=False):
+ scan_apk_aapt(apk, apk_file)
+ else:
+ scan_apk_androguard(apk, apk_file)
+
+ # Get the signature
+ logging.debug('Getting signature of {0}'.format(os.path.basename(apk_file)))
+ apk['sig'] = getsig(apk_file)
+ if not apk['sig']:
+ raise BuildException("Failed to get apk signature")
+
+ # Get size of the APK
+ apk['size'] = os.path.getsize(apk_file)
+
+ if 'minSdkVersion' not in apk:
+ logging.warning("No SDK version information found in {0}".format(apk_file))
+ apk['minSdkVersion'] = 1
+
+ # Check for known vulnerabilities
+ if has_known_vulnerability(apk_file):
+ apk['antiFeatures'].add('KnownVuln')
+
+ return apk
+
+
def scan_apk_aapt(apk, apkfile):
p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
if p.returncode != 0:
apk['features'].append(feature)
-def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=False,
- allow_disabled_algorithms=False, archive_bad_sig=False):
- """Scan the apk with the given filename in the given repo directory.
+def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=False,
+ allow_disabled_algorithms=False, archive_bad_sig=False):
+ """Processes the apk with the given filename in the given repo directory.
This also extracts the icons.
logging.critical("Spaces in filenames are not allowed.")
return True, None, False
+ apk = {}
apkfile = os.path.join(repodir, apkfilename)
- shasum = sha256sum(apkfile)
cachechanged = False
usecache = False
if apkfilename in apkcache:
apk = apkcache[apkfilename]
- if apk.get('hash') == shasum:
+ if apk.get('hash') == sha256sum(apkfile):
logging.debug("Reading " + apkfilename + " from cache")
usecache = True
else:
if not usecache:
logging.debug("Processing " + apkfilename)
- apk = {}
- apk['hash'] = shasum
- apk['hashType'] = 'sha256'
- apk['uses-permission'] = []
- apk['uses-permission-sdk-23'] = []
- apk['features'] = []
- apk['icons_src'] = {}
- apk['icons'] = {}
- apk['antiFeatures'] = set()
try:
- if SdkToolsPopen(['aapt', 'version'], output=False):
- scan_apk_aapt(apk, apkfile)
- else:
- scan_apk_androguard(apk, apkfile)
+ apk = scan_apk(apkfile)
except BuildException:
+ logging.warning('Skipping "%s" with invalid signature!', apkfilename)
return True, None, False
- if 'minSdkVersion' not in apk:
- logging.warn("No SDK version information found in {0}".format(apkfile))
- apk['minSdkVersion'] = 1
-
# Check for debuggable apks...
if common.isApkAndDebuggable(apkfile):
logging.warning('{0} is set to android:debuggable="true"'.format(apkfile))
- # Get the signature (or md5 of, to be precise)...
- logging.debug('Getting signature of {0}'.format(apkfile))
- apk['sig'] = getsig(os.path.join(os.getcwd(), apkfile))
- if not apk['sig']:
- logging.critical("Failed to get apk signature")
- return True, None, False
-
if options.rename_apks:
n = apk['packageName'] + '_' + str(apk['versionCode']) + '.apk'
std_short_name = os.path.join(repodir, n)
srcfilename = apkfilename[:-4] + "_src.tar.gz"
if os.path.exists(os.path.join(repodir, srcfilename)):
apk['srcname'] = srcfilename
- apk['size'] = os.path.getsize(apkfile)
# verify the jar signature is correct, allow deprecated
# algorithms only if the APK is in the archive.
logging.warning('Skipping "' + apkfilename + '" with invalid signature!')
return True, None, False
- if 'KnownVuln' not in apk['antiFeatures']:
- if has_known_vulnerability(apkfile):
- apk['antiFeatures'].add('KnownVuln')
-
apkzip = zipfile.ZipFile(apkfile, 'r')
# if an APK has files newer than the system time, suggest updating
dt_obj = datetime(*manifest.date_time)
checkdt = dt_obj - timedelta(1)
if datetime.today() < checkdt:
- logging.warn('System clock is older than manifest in: '
- + apkfilename
- + '\nSet clock to that time using:\n'
- + 'sudo date -s "' + str(dt_obj) + '"')
-
- iconfilename = "%s.%s.png" % (
- apk['packageName'],
- apk['versionCode'])
-
- # Extract the icon file...
- empty_densities = []
- for density in screen_densities:
- if density not in apk['icons_src']:
- empty_densities.append(density)
- continue
- iconsrc = apk['icons_src'][density]
- icon_dir = get_icon_dir(repodir, density)
- icondest = os.path.join(icon_dir, iconfilename)
-
- try:
- with open(icondest, 'wb') as f:
- f.write(get_icon_bytes(apkzip, iconsrc))
- apk['icons'][density] = iconfilename
- except (zipfile.BadZipFile, ValueError, KeyError) as e:
- logging.warning("Error retrieving icon file: %s" % (icondest))
- del apk['icons_src'][density]
- empty_densities.append(density)
-
- if '-1' in apk['icons_src']:
- iconsrc = apk['icons_src']['-1']
- iconpath = os.path.join(
- get_icon_dir(repodir, '0'), iconfilename)
- with open(iconpath, 'wb') as f:
- f.write(get_icon_bytes(apkzip, iconsrc))
- try:
- im = Image.open(iconpath)
- 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] = iconfilename
- shutil.move(iconpath,
- os.path.join(get_icon_dir(repodir, density), iconfilename))
- empty_densities.remove(density)
- break
- except Exception as e:
- logging.warn("Failed reading {0} - {1}".format(iconpath, e))
-
- if apk['icons']:
- apk['icon'] = iconfilename
-
- apkzip.close()
-
- # First try resizing down to not lose quality
- last_density = None
- for density in screen_densities:
- if density not in empty_densities:
- last_density = density
- continue
- if last_density is None:
- continue
- logging.debug("Density %s not available, resizing down from %s"
- % (density, last_density))
-
- last_iconpath = os.path.join(
- get_icon_dir(repodir, last_density), iconfilename)
- iconpath = os.path.join(
- get_icon_dir(repodir, density), iconfilename)
- fp = None
- try:
- fp = open(last_iconpath, 'rb')
- im = Image.open(fp)
-
- size = dpi_to_px(density)
-
- im.thumbnail((size, size), Image.ANTIALIAS)
- im.save(iconpath, "PNG")
- empty_densities.remove(density)
- except Exception as e:
- logging.warning("Invalid image file at %s: %s" % (last_iconpath, e))
- finally:
- if fp:
- fp.close()
-
- # Then just copy from the highest resolution available
- last_density = None
- for density in reversed(screen_densities):
- if density not in empty_densities:
- last_density = density
- continue
- if last_density is None:
- continue
- logging.debug("Density %s not available, copying from lower density %s"
- % (density, last_density))
-
- shutil.copyfile(
- os.path.join(get_icon_dir(repodir, last_density), iconfilename),
- os.path.join(get_icon_dir(repodir, density), iconfilename))
+ logging.warning('System clock is older than manifest in: '
+ + apkfilename
+ + '\nSet clock to that time using:\n'
+ + 'sudo date -s "' + str(dt_obj) + '"')
- empty_densities.remove(density)
-
- for density in screen_densities:
- icon_dir = get_icon_dir(repodir, density)
- icondest = os.path.join(icon_dir, iconfilename)
- resize_icon(icondest, density)
+ # extract icons from APK zip file
+ iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode'])
+ try:
+ empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir)
+ finally:
+ apkzip.close() # ensure that APK zip file gets closed
- # Copy from icons-mdpi to icons since mdpi is the baseline density
- baseline = os.path.join(get_icon_dir(repodir, '160'), iconfilename)
- if os.path.isfile(baseline):
- apk['icons']['0'] = iconfilename
- shutil.copyfile(baseline,
- os.path.join(get_icon_dir(repodir, '0'), iconfilename))
+ # resize existing icons for densities missing in the APK
+ fill_missing_icon_densities(empty_densities, iconfilename, apk, repodir)
if use_date_from_apk and manifest.date_time[1] != 0:
default_date_param = datetime(*manifest.date_time)
return False, apk, cachechanged
-def scan_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
- """Scan the apks in the given repo directory.
+def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
+ """Processes the apks in the given repo directory.
This also extracts the icons.
for apkfile in sorted(glob.glob(os.path.join(repodir, '*.apk'))):
apkfilename = apkfile[len(repodir) + 1:]
ada = options.allow_disabled_algorithms or config['allow_disabled_algorithms']
- (skip, apk, cachethis) = scan_apk(apkcache, apkfilename, repodir, knownapks,
- use_date_from_apk, ada, True)
+ (skip, apk, cachethis) = process_apk(apkcache, apkfilename, repodir, knownapks,
+ use_date_from_apk, ada, True)
if skip:
continue
apks.append(apk)
return apks, cachechanged
+def extract_apk_icons(icon_filename, apk, apk_zip, 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.
+
+ :param icon_filename: A string representing the icon's file name
+ :param apk: A populated dictionary containing APK metadata.
+ Needs to have 'icons_src' key
+ :param apk_zip: 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
+ """
+ empty_densities = []
+ for density in screen_densities:
+ if density not in apk['icons_src']:
+ empty_densities.append(density)
+ continue
+ icon_src = apk['icons_src'][density]
+ icon_dir = get_icon_dir(repo_dir, density)
+ icon_dest = os.path.join(icon_dir, icon_filename)
+
+ # Extract the icon files per density
+ try:
+ with open(icon_dest, 'wb') as f:
+ f.write(get_icon_bytes(apk_zip, icon_src))
+ apk['icons'][density] = icon_filename
+ 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']:
+ icon_src = apk['icons_src']['-1']
+ icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename)
+ with open(icon_path, 'wb') as f:
+ f.write(get_icon_bytes(apk_zip, icon_src))
+ 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 {0} - {1}".format(icon_path, e))
+
+ if apk['icons']:
+ apk['icon'] = icon_filename
+
+ 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
+
+ :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
+ """
+ # First try resizing down to not lose quality
+ last_density = None
+ for density in screen_densities:
+ if density not in empty_densities:
+ last_density = density
+ continue
+ if last_density is None:
+ continue
+ logging.debug("Density %s not available, resizing down from %s", density, last_density)
+
+ last_icon_path = os.path.join(get_icon_dir(repo_dir, last_density), icon_filename)
+ icon_path = os.path.join(get_icon_dir(repo_dir, density), icon_filename)
+ fp = None
+ try:
+ fp = open(last_icon_path, 'rb')
+ im = Image.open(fp)
+
+ size = dpi_to_px(density)
+
+ im.thumbnail((size, size), Image.ANTIALIAS)
+ im.save(icon_path, "PNG")
+ empty_densities.remove(density)
+ except Exception as e:
+ logging.warning("Invalid image file at %s: %s", last_icon_path, e)
+ finally:
+ if fp:
+ fp.close()
+
+ # Then just copy from the highest resolution available
+ last_density = None
+ for density in reversed(screen_densities):
+ if density not in empty_densities:
+ last_density = density
+ continue
+
+ if last_density is None:
+ continue
+
+ shutil.copyfile(
+ os.path.join(get_icon_dir(repo_dir, last_density), icon_filename),
+ os.path.join(get_icon_dir(repo_dir, density), icon_filename)
+ )
+ empty_densities.remove(density)
+
+ for density in screen_densities:
+ icon_dir = get_icon_dir(repo_dir, density)
+ icon_dest = os.path.join(icon_dir, icon_filename)
+ resize_icon(icon_dest, density)
+
+ # Copy from icons-mdpi to icons since mdpi is the baseline density
+ baseline = os.path.join(get_icon_dir(repo_dir, '160'), icon_filename)
+ if os.path.isfile(baseline):
+ apk['icons']['0'] = icon_filename
+ shutil.copyfile(baseline, os.path.join(get_icon_dir(repo_dir, '0'), icon_filename))
+
+
def apply_info_from_latest_apk(apps, apks):
"""
Some information from the apks needs to be applied up to the application level.
delete_disabled_builds(apps, apkcache, repodirs)
# Scan all apks in the main repo
- apks, cachechanged = scan_apks(apkcache, repodirs[0], knownapks, options.use_date_from_apk)
+ apks, cachechanged = process_apks(apkcache, repodirs[0], knownapks, options.use_date_from_apk)
files, fcachechanged = scan_repo_files(apkcache, repodirs[0], knownapks,
options.use_date_from_apk)
# Scan the archive repo for apks as well
if len(repodirs) > 1:
- archapks, cc = scan_apks(apkcache, repodirs[1], knownapks, options.use_date_from_apk)
+ archapks, cc = process_apks(apkcache, repodirs[1], knownapks, options.use_date_from_apk)
if cc:
cachechanged = True
else:
sys.path.insert(0, localmodule)
import fdroidserver.common
+import fdroidserver.exception
import fdroidserver.metadata
import fdroidserver.update
from fdroidserver.common import FDroidPopen
apps = fdroidserver.metadata.read_metadata(xref=True)
knownapks = fdroidserver.common.KnownApks()
- apks, cachechanged = fdroidserver.update.scan_apks({}, 'repo', knownapks, False)
+ apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
self.assertEqual(len(apks), 11)
apk = apks[0]
self.assertEqual(apk['packageName'], 'com.politedroid')
self.assertIsNone(apk.get('obbMainFile'))
self.assertIsNone(apk.get('obbPatchFile'))
- def testScanApkMetadata(self):
+ def test_scan_apk(self):
+ config = dict()
+ fdroidserver.common.fill_config_defaults(config)
+ fdroidserver.update.config = config
+ os.chdir(os.path.join(localmodule, 'tests'))
+ if os.path.basename(os.getcwd()) != 'tests':
+ 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',
+ '-1': 'res/drawable-mdpi-v4/icon_launcher.png'})
+ self.assertEqual(apk_info['icons'], {})
+ self.assertEqual(apk_info['features'], [])
+ self.assertEqual(apk_info['antiFeatures'], set())
+ self.assertEqual(apk_info['versionName'], 'v1.6pre2')
+ self.assertEqual(apk_info['hash'],
+ '897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8')
+ self.assertEqual(apk_info['packageName'], 'org.dyndns.fules.ck')
+ self.assertEqual(apk_info['versionCode'], 20)
+ self.assertEqual(apk_info['size'], 132453)
+ self.assertEqual(apk_info['nativecode'],
+ ['arm64-v8a', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'x86', 'x86_64'])
+ self.assertEqual(apk_info['minSdkVersion'], '7')
+ self.assertEqual(apk_info['sig'], '9bf7a6a67f95688daec75eab4b1436ac')
+ self.assertEqual(apk_info['hashType'], 'sha256')
+ self.assertEqual(apk_info['targetSdkVersion'], '8')
+
+ def test_scan_apk_no_sig(self):
+ config = dict()
+ fdroidserver.common.fill_config_defaults(config)
+ fdroidserver.update.config = config
+ os.chdir(os.path.join(localmodule, 'tests'))
+ if os.path.basename(os.getcwd()) != 'tests':
+ raise Exception('This test must be run in the "tests/" subdir')
+
+ with self.assertRaises(fdroidserver.exception.BuildException):
+ fdroidserver.update.scan_apk('urzip-release-unsigned.apk')
+
+ def test_process_apk(self):
def _build_yaml_representer(dumper, data):
'''Creates a YAML representation of a Build instance'''
apkList = ['../urzip.apk', '../org.dyndns.fules.ck_20.apk']
for apkName in apkList:
- _, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, False)
+ _, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo', knownapks,
+ False)
# Don't care about the date added to the repo and relative apkName
del apk['added']
del apk['apkName']
# avoid AAPT application name bug
del apk['name']
+ # ensure that icons have been extracted properly
+ if apkName == '../urzip.apk':
+ self.assertEqual(apk['icon'], 'info.guardianproject.urzip.100.png')
+ if apkName == '../org.dyndns.fules.ck_20.apk':
+ self.assertEqual(apk['icon'], 'org.dyndns.fules.ck.20.png')
+ for density in fdroidserver.update.screen_densities:
+ icon_path = os.path.join(fdroidserver.update.get_icon_dir('repo', density),
+ apk['icon'])
+ self.assertTrue(os.path.isfile(icon_path))
+ self.assertTrue(os.path.getsize(icon_path) > 1)
+
savepath = os.path.join('metadata', 'apk', apk['packageName'] + '.yaml')
# Uncomment to save APK metadata
# with open(savepath, 'w') as f:
self.maxDiff = None
self.assertEqual(apk, frompickle)
- def test_scan_apk_signed_by_disabled_algorithms(self):
+ def test_process_apk_signed_by_disabled_algorithms(self):
os.chdir(os.path.join(localmodule, 'tests'))
if os.path.basename(os.getcwd()) != 'tests':
raise Exception('This test must be run in the "tests/" subdir')
tmpdir = os.path.join(localmodule, '.testfiles')
if not os.path.exists(tmpdir):
os.makedirs(tmpdir)
- tmptestsdir = tempfile.mkdtemp(prefix='test_scan_apk_signed_by_disabled_algorithms-', dir=tmpdir)
+ tmptestsdir = tempfile.mkdtemp(prefix='test_process_apk_signed_by_disabled_algorithms-',
+ dir=tmpdir)
print('tmptestsdir', tmptestsdir)
os.chdir(tmptestsdir)
os.mkdir('repo')
os.mkdir('archive')
# setup the repo, create icons dirs, etc.
- fdroidserver.update.scan_apks({}, 'repo', knownapks)
- fdroidserver.update.scan_apks({}, 'archive', knownapks)
+ fdroidserver.update.process_apks({}, 'repo', knownapks)
+ fdroidserver.update.process_apks({}, 'archive', knownapks)
disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ]
for apkName in disabledsigs:
shutil.copy(os.path.join(apksourcedir, apkName),
os.path.join(tmptestsdir, 'repo'))
- skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
- allow_disabled_algorithms=True,
- archive_bad_sig=False)
+ skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
+ knownapks,
+ allow_disabled_algorithms=True,
+ archive_bad_sig=False)
self.assertFalse(skip)
self.assertIsNotNone(apk)
self.assertTrue(cachechanged)
# this test only works on systems with fully updated Java/jarsigner
# that has MD5 listed in jdk.jar.disabledAlgorithms in java.security
- skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
- allow_disabled_algorithms=False,
- archive_bad_sig=True)
+ skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
+ knownapks,
+ allow_disabled_algorithms=False,
+ archive_bad_sig=True)
self.assertTrue(skip)
self.assertIsNone(apk)
self.assertFalse(cachechanged)
self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
- skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'archive', knownapks,
- allow_disabled_algorithms=False,
- archive_bad_sig=False)
+ skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'archive',
+ knownapks,
+ allow_disabled_algorithms=False,
+ archive_bad_sig=False)
self.assertFalse(skip)
self.assertIsNotNone(apk)
self.assertTrue(cachechanged)
self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
+ # ensure that icons have been moved to the archive as well
+ for density in fdroidserver.update.screen_densities:
+ icon_path = os.path.join(fdroidserver.update.get_icon_dir('archive', density),
+ apk['icon'])
+ self.assertTrue(os.path.isfile(icon_path))
+ self.assertTrue(os.path.getsize(icon_path) > 1)
+
badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ]
for apkName in badsigs:
shutil.copy(os.path.join(apksourcedir, apkName),
os.path.join(tmptestsdir, 'repo'))
- skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
- allow_disabled_algorithms=False,
- archive_bad_sig=False)
+ skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
+ knownapks,
+ allow_disabled_algorithms=False,
+ archive_bad_sig=False)
self.assertTrue(skip)
self.assertIsNone(apk)
self.assertFalse(cachechanged)
- def test_scan_invalid_apk(self):
+ def test_process_invalid_apk(self):
os.chdir(os.path.join(localmodule, 'tests'))
if os.path.basename(os.getcwd()) != 'tests':
raise Exception('This test must be run in the "tests/" subdir')
knownapks = fdroidserver.common.KnownApks()
apk = 'fake.ota.update_1234.zip' # this is not an APK, scanning should fail
- (skip, apk, cachechanged) = fdroidserver.update.scan_apk({}, apk, 'repo', knownapks, False)
+ (skip, apk, cachechanged) = fdroidserver.update.process_apk({}, apk, 'repo', knownapks,
+ False)
self.assertTrue(skip)
self.assertIsNone(apk)