diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php
new file mode 100644
index 00000000000..48f3fe59582
--- /dev/null
+++ b/bridges/FDroidRepoBridge.php
@@ -0,0 +1,197 @@
+ array(
+ 'url' => array(
+ 'name' => 'Repository URL',
+ 'title' => 'Usually ends with /repo/',
+ 'required' => true,
+ 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo'
+ )
+ ),
+ 'Latest Updates' => array(
+ 'sorting' => array(
+ 'name' => 'Sort By',
+ 'type' => 'list',
+ 'values' => array(
+ 'Latest added apps' => 'added',
+ 'Latest updated apps' => 'lastUpdated'
+ )
+ ),
+ 'locale' => array(
+ 'name' => 'Locale',
+ 'defaultValue' => 'en-US'
+ )
+ ),
+ 'Follow Package' => array(
+ 'package' => array(
+ 'name' => 'Package Identifier',
+ 'required' => true,
+ 'exampleValue' => 'org.fox.ttrss'
+ )
+ )
+ );
+
+ // Stores repo information
+ private $repo;
+
+ public function getURI() {
+ if (empty($this->queriedContext))
+ return parent::getURI();
+
+ $url = rtrim($this->GetInput('url'), '/');
+ return strstr($url, '?', true) ?: $url;
+ }
+
+ public function getName() {
+ if (empty($this->queriedContext))
+ return parent::getName();
+
+ $name = $this->repo['repo']['name'];
+ switch($this->queriedContext) {
+ case 'Latest Updates':
+ return $name;
+ case 'Follow Package':
+ return $this->getInput('package') . ' - ' . $name;
+ default:
+ returnServerError('Unimplemented Context (getName)');
+ }
+ }
+
+ public function collectData() {
+ $this->repo = $this->getRepo();
+ switch($this->queriedContext) {
+ case 'Latest Updates':
+ $this->getAllUpdates();
+ break;
+ case 'Follow Package':
+ $this->getPackage($this->getInput('package'));
+ break;
+ default:
+ returnServerError('Unimplemented Context (collectData)');
+ }
+ }
+
+ private function getRepo() {
+ $url = $this->getURI();
+
+ // Get repo information (only available as JAR)
+ $jar = getContents($url . '/index-v1.jar');
+ $jar_loc = tempnam(sys_get_temp_dir(), '');
+ file_put_contents($jar_loc, $jar);
+
+ // JAR files are specially formatted ZIP files
+ $jar = new ZipArchive;
+ if ($jar->open($jar_loc) !== true) {
+ returnServerError('Failed to extract archive');
+ }
+
+ // Get file pointer to the relevant JSON inside
+ $fp = $jar->getStream('index-v1.json');
+ if (!$fp) {
+ returnServerError('Failed to get file pointer');
+ }
+
+ $data = json_decode(stream_get_contents($fp), true);
+ fclose($fp);
+ $jar->close();
+ return $data;
+ }
+
+ private function getAllUpdates() {
+ $apps = $this->repo['apps'];
+ usort($apps, function($a, $b) {
+ return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')];
+ });
+ $apps = array_slice($apps, 0, self::ITEM_LIMIT);
+ foreach($apps as $app) {
+ $latest = reset($this->repo['packages'][$app['packageName']]);
+
+ if (isset($app['localized'])) {
+ // Try provided locale, then en-US, then any
+ $lang = $app['localized'];
+ $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang);
+ } else
+ $lang = array();
+
+ $item = array();
+ $item['uri'] = $this->getURI() . '/' . $latest['apkName'];
+ $item['title'] = $lang['name'] ?? $app['packageName'];
+ $item['title'] .= ' ' . $latest['versionName'];
+ $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000));
+ if (isset($app['authorName']))
+ $item['author'] = $app['authorName'];
+ if (isset($app['categories']))
+ $item['categories'] = $app['categories'];
+
+ // Adding Content
+ $icon = $app['icon'] ?? '';
+ if (!empty($icon)) {
+ $icon = $this->getURI() . '/icons-320/' . $icon;
+ $item['enclosures'] = array($icon);
+ $icon = '';
+ }
+ $summary = $lang['summary'] ?? $app['summary'] ?? '';
+ $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None'));
+ $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None'));
+ $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null);
+ $source = $this->link($app['sourceCode'] ?? null);
+ $issueTracker = $this->link($app['issueTracker'] ?? null);
+ $license = $app['license'] ?? 'None';
+ $item['content'] = <<
Website: {$website}
+Source Code: {$source}
+Issue Tracker: {$issueTracker}
+license: {$app['license']}
+EOD; + $this->items[] = $item; + } + } + + private function getPackage($package) { + if (!isset($this->repo['packages'][$package])) { + returnClientError('Invalid Package Name'); + } + $package = $this->repo['packages'][$package]; + + $count = self::ITEM_LIMIT; + foreach($package as $version) { + $item = array(); + $item['uri'] = $this->getURI() . '/' . $version['apkName']; + $item['title'] = $version['versionName']; + $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000)); + $item['uid'] = $version['versionCode']; + $size = round($version['size'] / 1048576, 1); // Bytes -> MB + $sdk_link = 'https://developer.android.com/studio/releases/platforms'; + $item['content'] = <<Minimum SDK: {$version['minSdkVersion']} +(SDK to Android Version List)
+hash ({$version['hashType']}): {$version['hash']}
+EOD; + $this->items[] = $item; + if (--$count <= 0) + break; + } + } + + private function link($url) { + if (empty($url)) + return null; + return '' . $url . ''; + } +}