diff --git a/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java index ef5f05a13..08d475011 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java @@ -67,15 +67,15 @@ public class SwapRepoEmulatorTest { Preferences.setupForTests(context); FDroidApp.initWifiSettings(); - assertNull(FDroidApp.repo.address); + assertNull(FDroidApp.repo.getAddress()); final CountDownLatch latch = new CountDownLatch(1); new Thread() { @Override public void run() { - while (FDroidApp.repo.address == null) { + while (FDroidApp.repo.getAddress() == null) { try { - Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address); + Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.getAddress()); Thread.sleep(1000); } catch (InterruptedException e) { // ignored @@ -85,7 +85,7 @@ public class SwapRepoEmulatorTest { } }.start(); latch.await(10, TimeUnit.MINUTES); - assertNotNull(FDroidApp.repo.address); + assertNotNull(FDroidApp.repo.getAddress()); LocalRepoService.runProcess(context, new String[]{context.getPackageName()}); Log.i(TAG, "REPO: " + FDroidApp.repo); @@ -108,25 +108,24 @@ public class SwapRepoEmulatorTest { assertFalse(TextUtils.isEmpty(signingCert)); assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert))); - Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.getAddress()); while (repoToDelete != null) { Log.d(TAG, "Removing old test swap repo matching this one: " + repoToDelete.address); RepoProvider.Helper.remove(context, repoToDelete.getId()); - repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.getAddress()); } ContentValues values = new ContentValues(4); values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert); - values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.address); - values.put(Schema.RepoTable.Cols.NAME, FDroidApp.repo.name); + values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.getAddress()); + values.put(Schema.RepoTable.Cols.NAME, ""); values.put(Schema.RepoTable.Cols.IS_SWAP, true); final String lastEtag = UUID.randomUUID().toString(); values.put(Schema.RepoTable.Cols.LAST_ETAG, lastEtag); RepoProvider.Helper.insert(context, values); - Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.getAddress()); assertTrue(repo.isSwap); assertNotEquals(-1, repo.getId()); - assertTrue(repo.name.startsWith(FDroidApp.repo.name)); assertEquals(lastEtag, repo.lastetag); assertNull(repo.lastUpdated); @@ -136,7 +135,7 @@ public class SwapRepoEmulatorTest { updater.update(); assertTrue(updater.hasChanged()); - repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.getAddress()); final Date lastUpdated = repo.lastUpdated; assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java b/app/src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java index 8c00f8412..ae0a15916 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java @@ -1,11 +1,13 @@ package org.fdroid.fdroid.nearby; +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; @@ -19,6 +21,7 @@ import org.fdroid.fdroid.nearby.peers.BluetoothPeer; import java.lang.ref.WeakReference; +import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; /** @@ -72,6 +75,11 @@ public class BluetoothManager { * so make sure {@link android.content.BroadcastReceiver}s handle duplicates. */ public static void start(final Context context) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != + PackageManager.PERMISSION_GRANTED) { + // TODO we either throw away that Bluetooth code or properly request permissions here + return; + } BluetoothManager.context = new WeakReference<>(context); if (handlerThread != null && handlerThread.isAlive()) { sendBroadcast(STATUS_STARTED, null); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/BonjourManager.java b/app/src/full/java/org/fdroid/fdroid/nearby/BonjourManager.java index 1f82b53fa..1279d3250 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/BonjourManager.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/BonjourManager.java @@ -181,7 +181,7 @@ public class BonjourManager { HashMap values = new HashMap<>(); values.put(BonjourPeer.PATH, "/fdroid/repo"); values.put(BonjourPeer.NAME, localRepoName); - values.put(BonjourPeer.FINGERPRINT, FDroidApp.repo.fingerprint); + values.put(BonjourPeer.FINGERPRINT, FDroidApp.repo.getFingerprint()); String type; if (useHttps) { values.put(BonjourPeer.TYPE, "fdroidrepos"); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java index 8105baa28..61aae7960 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java @@ -4,28 +4,20 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; import android.util.Log; +import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.Hasher; -import org.fdroid.fdroid.IndexUpdater; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.InstalledApp; -import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.SanitizedFile; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; -import org.xmlpull.v1.XmlSerializer; +import org.fdroid.index.v1.AppV1; +import org.fdroid.index.v1.IndexV1; +import org.fdroid.index.v1.IndexV1Creator; +import org.fdroid.index.v1.PackageV1; +import org.fdroid.index.v1.RepoV1; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -37,15 +29,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.security.cert.CertificateEncodingException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -72,16 +61,15 @@ public final class LocalRepoManager { "swap-tick-not-done.png", }; - private final Map apps = new ConcurrentHashMap<>(); + private final List apps = new ArrayList<>(); - private final SanitizedFile xmlIndexJar; - private final SanitizedFile xmlIndexJarUnsigned; + private final SanitizedFile indexJar; + private final SanitizedFile indexJarUnsigned; private final SanitizedFile webRoot; private final SanitizedFile fdroidDir; private final SanitizedFile fdroidDirCaps; private final SanitizedFile repoDir; private final SanitizedFile repoDirCaps; - private final SanitizedFile iconsDir; @Nullable private static LocalRepoManager localRepoManager; @@ -106,9 +94,8 @@ public final class LocalRepoManager { fdroidDirCaps = new SanitizedFile(webRoot, "FDROID"); repoDir = new SanitizedFile(fdroidDir, "repo"); repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO"); - iconsDir = new SanitizedFile(repoDir, "icons"); - xmlIndexJar = new SanitizedFile(repoDir, IndexUpdater.SIGNED_FILE_NAME); - xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar"); + indexJar = new SanitizedFile(repoDir, "index-v1.jar"); + indexJarUnsigned = new SanitizedFile(repoDir, "index-v1.unsigned.jar"); if (!fdroidDir.exists() && !fdroidDir.mkdir()) { Log.e(TAG, "Unable to create empty base: " + fdroidDir); @@ -118,6 +105,7 @@ public final class LocalRepoManager { Log.e(TAG, "Unable to create empty repo: " + repoDir); } + SanitizedFile iconsDir = new SanitizedFile(repoDir, "icons"); if (!iconsDir.exists() && !iconsDir.mkdir()) { Log.e(TAG, "Unable to create icons folder: " + iconsDir); } @@ -141,7 +129,7 @@ public final class LocalRepoManager { return fdroidClientURL; } - public void writeIndexPage(String repoAddress) { + void writeIndexPage(String repoAddress) { final String fdroidClientURL = writeFdroidApkToWebroot(); try { File indexHtml = new File(webRoot, "index.html"); @@ -151,10 +139,13 @@ public final class LocalRepoManager { new FileOutputStream(indexHtml))); StringBuilder builder = new StringBuilder(); - for (App app : apps.values()) { + for (App app : apps) { builder.append("
  • ") + .append("(apps.keySet())); + void generateIndex(String address, String[] selectedApps) throws IOException { + String name = Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString; + String description = "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName(); + RepoV1 repo = new RepoV1(System.currentTimeMillis(), 20001, 7, name, "swap-icon.png", address, description, Collections.emptyList()); + Set apps = new HashSet<>(Arrays.asList(selectedApps)); + IndexV1Creator creator = new IndexV1Creator(context.getPackageManager(), repoDir, apps, repo); + IndexV1 indexV1 = creator.createRepo(); + cacheApps(indexV1); + writeIndexPage(address); + SanitizedFile indexJson = new SanitizedFile(repoDir, "index-v1.json"); + writeIndexJar(indexJson); } - private void copyApksToRepo(List appsToCopy) { - for (final String packageName : appsToCopy) { - final App app = apps.get(packageName); - - if (app.installedApk != null) { - SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName); - if (Utils.symlinkOrCopyFileQuietly(app.installedApk.installedFile, outFile)) { - continue; - } + private void cacheApps(IndexV1 indexV1) { + this.apps.clear(); + for (AppV1 a : indexV1.getApps()) { + App app = new App(); + app.packageName = a.getPackageName(); + app.name = a.getName(); + app.installedApk = new Apk(); + List packages = indexV1.getPackages().get(a.getPackageName()); + if (packages != null && packages.size() > 0) { + Long versionCode = packages.get(0).getVersionCode(); + if (versionCode != null) app.installedApk.versionCode = versionCode; } - // if we got here, something went wrong - throw new IllegalStateException("Unable to copy APK"); + this.apps.add(app); } } - public void addApp(Context context, String packageName) { - App app = null; - try { - InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName); - app = App.getInstance(context, pm, installedApp, packageName); - if (app == null || !app.isValid()) { - return; - } - } catch (PackageManager.NameNotFoundException | CertificateEncodingException | IOException e) { - Log.e(TAG, "Error adding app to local repo", e); - return; - } - Utils.debugLog(TAG, "apps.put: " + packageName); - apps.put(packageName, app); - } - - public void copyIconsToRepo() { - ApplicationInfo appInfo; - for (final App app : apps.values()) { - if (app.installedApk != null) { - try { - appInfo = pm.getApplicationInfo(app.packageName, PackageManager.GET_META_DATA); - copyIconToRepo(appInfo.loadIcon(pm), app.packageName, app.installedApk.versionCode); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Error getting app icon", e); - } - } - } - } - - /** - * Extracts the icon from an APK and writes it to the repo as a PNG - */ - private void copyIconToRepo(Drawable drawable, String packageName, long versionCode) { - Bitmap bitmap; - if (drawable instanceof BitmapDrawable) { - bitmap = ((BitmapDrawable) drawable).getBitmap(); - } else { - bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - } - File png = getIconFile(packageName, versionCode); - OutputStream out; - try { - out = new BufferedOutputStream(new FileOutputStream(png)); - bitmap.compress(CompressFormat.PNG, 100, out); - out.close(); - } catch (Exception e) { - Log.e(TAG, "Error copying icon to repo", e); - } - } - - private File getIconFile(String packageName, long versionCode) { - return new File(iconsDir, App.getIconName(packageName, versionCode)); - } - - /** - * Helper class to aid in constructing index.xml file. - */ - public static final class IndexXmlBuilder { - @NonNull - private final XmlSerializer serializer; - - @NonNull - private final DateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - - private IndexXmlBuilder() throws XmlPullParserException { - serializer = XmlPullParserFactory.newInstance().newSerializer(); - } - - public void build(Context context, Map apps, OutputStream output) - throws IOException, LocalRepoKeyStore.InitException { - serializer.setOutput(output, "UTF-8"); - serializer.startDocument(null, null); - serializer.startTag("", "fdroid"); - - // block - serializer.startTag("", "repo"); - serializer.attribute("", "icon", "blah.png"); - serializer.attribute("", "name", Preferences.get().getLocalRepoName() - + " on " + FDroidApp.ipAddressString); - serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); - long timestamp = System.currentTimeMillis() / 1000L; - serializer.attribute("", "timestamp", String.valueOf(timestamp)); - serializer.attribute("", "version", "10"); - tag("description", "A local FDroid repo generated from apps installed on " - + Preferences.get().getLocalRepoName()); - serializer.endTag("", "repo"); - - // blocks - for (Map.Entry entry : apps.entrySet()) { - tagApplication(entry.getValue()); - } - - serializer.endTag("", "fdroid"); - serializer.endDocument(); - output.close(); - } - - /** - * Helper function to start a tag called "name", fill it with text "text", and then - * end the tag in a more concise manner. If "text" is blank, skip the tag entirely. - */ - private void tag(String name, String text) throws IOException { - if (TextUtils.isEmpty(text)) { - return; - } - serializer.startTag("", name).text(text).endTag("", name); - } - - /** - * Alias for {@link org.fdroid.fdroid.nearby.LocalRepoManager.IndexXmlBuilder#tag(String, String)} - * That accepts a number instead of string. - * - * @see IndexXmlBuilder#tag(String, String) - */ - private void tag(String name, long number) throws IOException { - tag(name, String.valueOf(number)); - } - - /** - * Alias for {@link org.fdroid.fdroid.nearby.LocalRepoManager.IndexXmlBuilder#tag(String, String)} - * that accepts a date instead of a string. - * - * @see IndexXmlBuilder#tag(String, String) - */ - private void tag(String name, Date date) throws IOException { - tag(name, dateToStr.format(date)); - } - - private void tagApplication(App app) throws IOException { - serializer.startTag("", "application"); - serializer.attribute("", "id", app.packageName); - - tag("id", app.packageName); - tag("added", app.added); - tag("lastupdated", app.lastUpdated); - tag("name", app.name); - tag("summary", app.summary); - tag("icon", app.iconFromApk); - tag("desc", app.description); - tag("license", "Unknown"); - tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName()); - tag("category", "LocalRepo," + Preferences.get().getLocalRepoName()); - tag("web", "web"); - tag("source", "source"); - tag("tracker", "tracker"); - tag("marketversion", app.installedApk.versionName); - tag("marketvercode", app.installedApk.versionCode); - - tagPackage(app); - - serializer.endTag("", "application"); - } - - private void tagPackage(App app) throws IOException { - serializer.startTag("", "package"); - - tag("version", app.installedApk.versionName); - tag("versioncode", app.installedApk.versionCode); - tag("apkname", app.installedApk.apkName); - tagHash(app); - tag("sig", app.installedApk.sig.toLowerCase(Locale.US)); - tag("size", app.installedApk.installedFile.length()); - tag("added", app.installedApk.added); - if (app.installedApk.minSdkVersion > Apk.SDK_VERSION_MIN_VALUE) { - tag("sdkver", app.installedApk.minSdkVersion); - } - if (app.installedApk.targetSdkVersion > app.installedApk.minSdkVersion) { - tag("targetSdkVersion", app.installedApk.targetSdkVersion); - } - if (app.installedApk.maxSdkVersion < Apk.SDK_VERSION_MAX_VALUE) { - tag("maxsdkver", app.installedApk.maxSdkVersion); - } - tagFeatures(app); - tagPermissions(app); - tagNativecode(app); - - serializer.endTag("", "package"); - } - - private void tagPermissions(App app) throws IOException { - serializer.startTag("", "permissions"); - if (app.installedApk.requestedPermissions != null) { - StringBuilder buff = new StringBuilder(); - - for (String permission : app.installedApk.requestedPermissions) { - buff.append(permission.replace("android.permission.", "")); - buff.append(','); - } - String out = buff.toString(); - if (!TextUtils.isEmpty(out)) { - serializer.text(out.substring(0, out.length() - 1)); - } - } - serializer.endTag("", "permissions"); - } - - private void tagFeatures(App app) throws IOException { - serializer.startTag("", "features"); - if (app.installedApk.features != null) { - serializer.text(TextUtils.join(",", app.installedApk.features)); - } - serializer.endTag("", "features"); - } - - private void tagNativecode(App app) throws IOException { - if (app.installedApk.nativecode != null) { - serializer.startTag("", "nativecode"); - serializer.text(TextUtils.join(",", app.installedApk.nativecode)); - serializer.endTag("", "nativecode"); - } - } - - private void tagHash(App app) throws IOException { - serializer.startTag("", "hash"); - serializer.attribute("", "type", app.installedApk.hashType); - serializer.text(app.installedApk.hash); - serializer.endTag("", "hash"); - } - } - - public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException { - BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned)); + private void writeIndexJar(SanitizedFile indexJson) throws IOException { + BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(indexJarUnsigned)); JarOutputStream jo = new JarOutputStream(bo); - JarEntry je = new JarEntry(IndexUpdater.DATA_FILE_NAME); + JarEntry je = new JarEntry(indexJson.getName()); jo.putNextEntry(je); - new IndexXmlBuilder().build(context, apps, jo); + FileUtils.copyFile(indexJson, jo); jo.close(); bo.close(); try { - LocalRepoKeyStore.get(context).signZip(xmlIndexJarUnsigned, xmlIndexJar); + LocalRepoKeyStore.get(context).signZip(indexJarUnsigned, indexJar); } catch (LocalRepoKeyStore.InitException e) { throw new IOException("Could not sign index - keystore failed to initialize"); } finally { - attemptToDelete(xmlIndexJarUnsigned); + attemptToDelete(indexJarUnsigned); } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoService.java b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoService.java index 882310276..bcf0e3f19 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoService.java @@ -4,13 +4,12 @@ import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.os.Process; +import android.util.Log; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.xmlpull.v1.XmlPullParserException; -import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Set; @@ -103,30 +102,13 @@ public class LocalRepoService extends IntentService { final LocalRepoManager lrm = LocalRepoManager.get(context); broadcast(context, STATUS_PROGRESS, R.string.deleting_repo); lrm.deleteRepo(); - for (String app : selectedApps) { - broadcast(context, STATUS_PROGRESS, context.getString(R.string.adding_apks_format, app)); - lrm.addApp(context, app); - } - String urlString = Utils.getSharingUri(FDroidApp.repo).toString(); - lrm.writeIndexPage(urlString); - broadcast(context, STATUS_PROGRESS, R.string.writing_index_jar); - lrm.writeIndexJar(); broadcast(context, STATUS_PROGRESS, R.string.linking_apks); - lrm.copyApksToRepo(); - broadcast(context, STATUS_PROGRESS, R.string.copying_icons); - // run the icon copy without progress, its not a blocker - new Thread() { - @Override - public void run() { - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); - lrm.copyIconsToRepo(); - } - }.start(); - + String urlString = Utils.getSharingUri(FDroidApp.repo).toString(); + lrm.generateIndex(urlString, selectedApps); broadcast(context, STATUS_STARTED, null); - } catch (IOException | XmlPullParserException | LocalRepoKeyStore.InitException e) { + } catch (Exception e) { broadcast(context, STATUS_ERROR, e.getLocalizedMessage()); - e.printStackTrace(); + Log.e(TAG, "Error creating repo", e); } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java b/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java index 6890230c0..62dee5a91 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java @@ -1,12 +1,12 @@ package org.fdroid.fdroid.nearby; +import static java.util.Objects.requireNonNull; + import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.database.Cursor; import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ContextThemeWrapper; @@ -14,25 +14,26 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.data.Schema.InstalledAppTable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.cursoradapter.widget.CursorAdapter; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; -public class SelectAppsView extends SwapView implements LoaderManager.LoaderCallbacks { +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.InstalledAppProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class SelectAppsView extends SwapView { public SelectAppsView(Context context) { super(context); @@ -58,26 +59,28 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall protected void onFinishInflate() { super.onFinishInflate(); listView = findViewById(R.id.list); - adapter = new AppListAdapter(listView, getContext(), - getContext().getContentResolver().query(InstalledAppProvider.getContentUri(), - null, null, null, null)); + List packages = getContext().getPackageManager().getInstalledPackages(0); + adapter = new AppListAdapter(listView, packages); listView.setAdapter(adapter); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - // either reconnect with an existing loader or start a new one - getActivity().getSupportLoaderManager().initLoader(R.layout.swap_select_apps, null, this); - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { toggleAppSelected(position); } }); + afterAppsLoaded(); + } + + @Override + public void setCurrentFilterString(String currentFilterString) { + super.setCurrentFilterString(currentFilterString); + adapter.setSearchTerm(currentFilterString); } private void toggleAppSelected(int position) { - Cursor c = (Cursor) adapter.getItem(position); - String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); + String packageName = adapter.getItem(position).packageName; if (getActivity().getSwapService().hasSelectedPackage(packageName)) { getActivity().getSwapService().deselectPackage(packageName); adapter.updateCheckedIndicatorView(position, false); @@ -88,40 +91,21 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall LocalRepoService.create(getContext(), getActivity().getSwapService().getAppsToSwap()); } - @Override - public CursorLoader onCreateLoader(int id, Bundle args) { - Uri uri; - if (TextUtils.isEmpty(currentFilterString)) { - uri = InstalledAppProvider.getContentUri(); - } else { - uri = InstalledAppProvider.getSearchUri(currentFilterString); - } - return new CursorLoader(getActivity(), uri, null, null, null, null); - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - adapter.swapCursor(cursor); - + public void afterAppsLoaded() { for (int i = 0; i < listView.getCount(); i++) { - Cursor c = (Cursor) listView.getItemAtPosition(i); - String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); + InstalledApp app = (InstalledApp) listView.getItemAtPosition(i); getActivity().getSwapService().ensureFDroidSelected(); for (String selected : getActivity().getSwapService().getAppsToSwap()) { - if (TextUtils.equals(packageName, selected)) { + if (TextUtils.equals(app.packageName, selected)) { listView.setItemChecked(i, true); } } } } - @Override - public void onLoaderReset(Loader loader) { - adapter.swapCursor(null); - } - - private class AppListAdapter extends CursorAdapter { + private class AppListAdapter extends BaseAdapter { + private final Context context = SelectAppsView.this.getContext(); @Nullable private LayoutInflater inflater; @@ -131,9 +115,31 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall @NonNull private final ListView listView; - AppListAdapter(@NonNull ListView listView, @NonNull Context context, @Nullable Cursor c) { - super(context, c, FLAG_REGISTER_CONTENT_OBSERVER); + private final List allPackages; + private final List filteredPackages = new ArrayList<>(); + + AppListAdapter(@NonNull ListView listView, List packageInfos) { this.listView = listView; + allPackages = new ArrayList<>(packageInfos.size()); + for (PackageInfo packageInfo : packageInfos) { + allPackages.add(new InstalledApp(context, packageInfo)); + } + filteredPackages.addAll(allPackages); + } + + void setSearchTerm(@Nullable String searchTerm) { + filteredPackages.clear(); + if (TextUtils.isEmpty(searchTerm)) { + filteredPackages.addAll(allPackages); + } else { + String query = requireNonNull(searchTerm).toLowerCase(Locale.US); + for (InstalledApp app: allPackages) { + if (app.name.toLowerCase(Locale.US).contains(query)) { + filteredPackages.add(app); + } + } + } + notifyDataSetChanged(); } @NonNull @@ -153,35 +159,32 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = getInflater(context).inflate(R.layout.select_local_apps_list_item, parent, false); - bindView(view, context, cursor); + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView == null ? + getInflater(context).inflate(R.layout.select_local_apps_list_item, parent, false) : + convertView; + bindView(view, context, position); return view; } - @Override - public void bindView(final View view, final Context context, final Cursor cursor) { + public void bindView(final View view, final Context context, final int position) { + InstalledApp app = getItem(position); TextView packageView = (TextView) view.findViewById(R.id.package_name); TextView labelView = (TextView) view.findViewById(R.id.application_label); ImageView iconView = (ImageView) view.findViewById(android.R.id.icon); - String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); - String appLabel = cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.APPLICATION_LABEL)); - Drawable icon; try { - icon = context.getPackageManager().getApplicationIcon(packageName); + icon = context.getPackageManager().getApplicationIcon(app.packageName); } catch (PackageManager.NameNotFoundException e) { icon = getDefaultAppIcon(context); } - packageView.setText(packageName); - labelView.setText(appLabel); + packageView.setText(app.packageName); + labelView.setText(app.name); iconView.setImageDrawable(icon); - final int listPosition = cursor.getPosition(); - // Since v11, the Android SDK provided the ability to show selected list items // by highlighting their background. Prior to this, we need to handle this ourselves // by adding a checkbox which can toggle selected items. @@ -190,12 +193,12 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall CheckBox checkBox = (CheckBox) checkBoxView; checkBox.setOnCheckedChangeListener(null); - checkBox.setChecked(listView.isItemChecked(listPosition)); + checkBox.setChecked(listView.isItemChecked(position)); checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - listView.setItemChecked(listPosition, isChecked); - toggleAppSelected(listPosition); + listView.setItemChecked(position, isChecked); + toggleAppSelected(position); } }); } @@ -209,5 +212,35 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall final int childIndex = position - firstListItemPosition; } } + + @Override + public int getCount() { + return filteredPackages.size(); + } + + @Override + public InstalledApp getItem(int position) { + return filteredPackages.get(position); + } + + @Override + public long getItemId(int position) { + return getItem(position).hashCode(); + } } + + private static class InstalledApp { + final String packageName; + final String name; + + InstalledApp(String packageName, String name) { + this.packageName = packageName; + this.name = name; + } + + InstalledApp(Context context, PackageInfo packageInfo) { + this(packageInfo.packageName, Utils.getApplicationLabel(context, packageInfo.packageName)); + } + } + } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/StartSwapView.java b/app/src/full/java/org/fdroid/fdroid/nearby/StartSwapView.java index 6f3b1c78f..51cf6d0a8 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/StartSwapView.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/StartSwapView.java @@ -1,11 +1,13 @@ package org.fdroid.fdroid.nearby; +import android.Manifest; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.net.wifi.WifiConfiguration; import android.text.TextUtils; import android.util.AttributeSet; @@ -178,7 +180,8 @@ public class StartSwapView extends SwapView { } private void uiInitBluetooth() { - if (bluetooth != null) { + if (bluetooth != null && ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { viewBluetoothId = (TextView) findViewById(R.id.device_id_bluetooth); viewBluetoothId.setText(bluetooth.getName()); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java index 56ee7e239..358cde6f0 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java @@ -5,7 +5,6 @@ import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -16,18 +15,27 @@ import android.os.IBinder; import android.text.TextUtils; import android.util.Log; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import org.fdroid.database.Repository; +import org.fdroid.download.Downloader; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NotificationHelper; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.nearby.peers.Peer; +import org.fdroid.fdroid.net.DownloaderFactory; import org.fdroid.fdroid.net.DownloaderService; +import org.fdroid.index.IndexParser; +import org.fdroid.index.IndexParserKt; +import org.fdroid.index.SigningException; +import org.fdroid.index.v1.IndexV1; +import org.fdroid.index.v1.IndexV1Verifier; +import java.io.File; +import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; @@ -49,6 +57,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import kotlin.Pair; /** * Central service which manages all of the different moving parts of swap @@ -71,6 +80,8 @@ public class SwapService extends Service { @NonNull private final Set appsToSwap = new HashSet<>(); private final Set activePeers = new HashSet<>(); + private final MutableLiveData index = new MutableLiveData<>(); + private final MutableLiveData indexError = new MutableLiveData<>(); private static LocalBroadcastManager localBroadcastManager; private static SharedPreferences swapPreferences; @@ -103,46 +114,57 @@ public class SwapService extends Service { } } - public void connectTo(@NonNull Peer peer) { + private void connectTo(@NonNull Peer peer) { if (peer != this.peer) { Log.e(TAG, "Oops, got a different peer to swap with than initially planned."); } peerRepo = ensureRepoExists(peer); - UpdateService.updateRepoNow(this, peer.getRepoAddress()); + try { + updateRepo(peer, peerRepo); + } catch (Exception e) { + Log.e(TAG, "Error updating repo.", e); + indexError.postValue(e); + } } - private Repo ensureRepoExists(@NonNull Peer peer) { - // TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with - // the repos address in the database. Not sure on best behaviour in this situation. - Repo repo = RepoProvider.Helper.findByAddress(this, peer.getRepoAddress()); - if (repo == null) { - ContentValues values = new ContentValues(6); - - // The name/description is not really required, as swap repos are not shown in the - // "Manage repos" UI on other device. Doesn't hurt to put something there though, - // on the off chance that somebody is looking through the sqlite database which - // contains the repos... - values.put(Schema.RepoTable.Cols.NAME, peer.getName()); - values.put(Schema.RepoTable.Cols.ADDRESS, peer.getRepoAddress()); - values.put(Schema.RepoTable.Cols.DESCRIPTION, ""); - String fingerprint = peer.getFingerprint(); - if (!TextUtils.isEmpty(fingerprint)) { - values.put(Schema.RepoTable.Cols.FINGERPRINT, peer.getFingerprint()); - } - values.put(Schema.RepoTable.Cols.IN_USE, 1); - values.put(Schema.RepoTable.Cols.IS_SWAP, true); - Uri uri = RepoProvider.Helper.insert(this, values); - repo = RepoProvider.Helper.get(this, uri); + private void updateRepo(@NonNull Peer peer, Repository repo) + throws IOException, InterruptedException, SigningException { + Uri uri = Uri.parse(repo.getAddress()).buildUpon().appendPath("index-v1.jar").build(); + File swapJarFile = + File.createTempFile("swap", "", getApplicationContext().getCacheDir()); + try { + Downloader downloader = + DownloaderFactory.INSTANCE.createWithTryFirstMirror(repo, uri, swapJarFile); + downloader.download(); + IndexV1Verifier verifier = new IndexV1Verifier(swapJarFile, null, peer.getFingerprint()); + Pair pair = verifier.getStreamAndVerify(inputStream -> + IndexParserKt.parseV1(IndexParser.INSTANCE, inputStream) + ); + index.postValue(pair.getSecond()); + startPollingConnectedSwapRepo(); + } finally { + //noinspection ResultOfMethodCallIgnored + swapJarFile.delete(); } + } - return repo; + private Repository ensureRepoExists(@NonNull Peer peer) { + return FDroidApp.createSwapRepo(peer.getRepoAddress(), null); } @Nullable - public Repo getPeerRepo() { + public Repository getPeerRepo() { return peerRepo; } + public LiveData getIndex() { + return index; + } + + public LiveData getIndexError() { + return indexError; + } + // ================================================= // Have selected a specific peer to swap with // (Rather than showing a generic QR code to scan) @@ -152,7 +174,7 @@ public class SwapService extends Service { private Peer peer; @Nullable - private Repo peerRepo; + private Repository peerRepo; public void swapWith(Peer peer) { this.peer = peer; @@ -341,11 +363,9 @@ public class SwapService extends Service { Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); localBroadcastManager.registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); - localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS)); localBroadcastManager.registerReceiver(bluetoothPeerFound, new IntentFilter(BluetoothManager.ACTION_FOUND)); localBroadcastManager.registerReceiver(bonjourPeerFound, new IntentFilter(BonjourManager.ACTION_FOUND)); localBroadcastManager.registerReceiver(bonjourPeerRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED)); - localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS)); if (getHotspotActivatedUserPreference()) { WifiApControl wifiApControl = WifiApControl.getInstance(this); @@ -362,13 +382,13 @@ public class SwapService extends Service { BonjourManager.setVisible(this, getWifiVisibleUserPreference() || getHotspotActivatedUserPreference()); } - private void askServerToSwapWithUs(final Repo repo) { + private void askServerToSwapWithUs(final Repository repo) { compositeDisposable.add( Completable.fromAction(() -> { String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString(); HttpURLConnection conn = null; try { - URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap")); + URL url = new URL(repo.getAddress().replace("/fdroid/repo", "/request-swap")); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoInput(true); @@ -381,7 +401,7 @@ public class SwapService extends Service { } int responseCode = conn.getResponseCode(); - Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " + + Utils.debugLog(TAG, "Asking server at " + repo.getAddress() + " to swap with us in return (by " + "POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode); } finally { if (conn != null) { @@ -393,7 +413,7 @@ public class SwapService extends Service { .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete(e -> { Intent intent = new Intent(DownloaderService.ACTION_INTERRUPTED); - intent.setData(Uri.parse(repo.address)); + intent.setData(Uri.parse(repo.getAddress())); intent.putExtra(DownloaderService.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage()); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); return true; @@ -409,7 +429,6 @@ public class SwapService extends Service { */ @Override public int onStartCommand(Intent intent, int flags, int startId) { - deleteAllSwapRepos(); Intent startUiIntent = new Intent(this, SwapWorkflowActivity.class); startUiIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(startUiIntent); @@ -430,7 +449,6 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); localBroadcastManager.unregisterReceiver(onWifiChange); - localBroadcastManager.unregisterReceiver(bluetoothStatus); localBroadcastManager.unregisterReceiver(bluetoothPeerFound); localBroadcastManager.unregisterReceiver(bonjourPeerFound); localBroadcastManager.unregisterReceiver(bonjourPeerRemoved); @@ -467,8 +485,6 @@ public class SwapService extends Service { } ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); - deleteAllSwapRepos(); - super.onDestroy(); } @@ -484,24 +500,6 @@ public class SwapService extends Service { .build(); } - /** - * For now, swap repos are only trusted as long as swapping is active. They - * should have a long lived trust based on the signing key, but that requires - * that the repos are stored in the database by fingerprint, not by URL address. - * - * @see TOFU in swap - * @see - * signing key fingerprint should be sole ID for repos in the database - */ - private void deleteAllSwapRepos() { - for (Repo repo : RepoProvider.Helper.all(this)) { - if (repo.isSwap) { - Utils.debugLog(TAG, "Removing stale swap repo: " + repo.address + " - " + repo.fingerprint); - RepoProvider.Helper.remove(this, repo.getId()); - } - } - } - private void startPollingConnectedSwapRepo() { stopPollingConnectedSwapRepo(); pollConnectedSwapRepoTimer = new Timer("pollConnectedSwapRepoTimer", true); @@ -572,44 +570,6 @@ public class SwapService extends Service { } }; - private final BroadcastReceiver bluetoothStatus = new SwapStateChangeReceiver(); - private final BroadcastReceiver localRepoStatus = new SwapStateChangeReceiver(); - - /** - * When swapping is setup, then start the index polling. - */ - private class SwapStateChangeReceiver extends BroadcastReceiver { - private final BroadcastReceiver pollForUpdatesReceiver = new PollForUpdatesReceiver(); - - @Override - public void onReceive(Context context, Intent intent) { - int bluetoothStatus = intent.getIntExtra(BluetoothManager.ACTION_STATUS, -1); - int wifiStatus = intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1); - if (bluetoothStatus == BluetoothManager.STATUS_STARTED - || wifiStatus == LocalRepoService.STATUS_STARTED) { - localBroadcastManager.registerReceiver(pollForUpdatesReceiver, - new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); - } else { - localBroadcastManager.unregisterReceiver(pollForUpdatesReceiver); - } - } - } - - /** - * Reschedule an index update if the last one was successful. - */ - private class PollForUpdatesReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - switch (intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1)) { - case UpdateService.STATUS_COMPLETE_AND_SAME: - case UpdateService.STATUS_COMPLETE_WITH_CHANGES: - startPollingConnectedSwapRepo(); - break; - } - } - } - /** * Handle events if the user or system changes the Bluetooth setup outside of F-Droid. */ diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java index f3573d650..b2b40c294 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java @@ -6,12 +6,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.database.ContentObserver; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -19,38 +15,36 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; -import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; -import androidx.cursoradapter.widget.CursorAdapter; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import org.fdroid.database.Repository; +import org.fdroid.fdroid.CompatibilityChecker; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; -import org.fdroid.download.Downloader; import org.fdroid.fdroid.net.DownloaderService; +import org.fdroid.index.v1.AppV1; +import org.fdroid.index.v1.IndexV1; +import org.fdroid.index.v1.PackageV1; +import org.fdroid.index.v1.PermissionV1; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This is a view that shows a listing of all apps in the swap repo that this @@ -58,7 +52,7 @@ import java.util.List; * {@link org.fdroid.fdroid.views.apps.AppListActivity}'s plumbing. */ // TODO merge this with AppListActivity, perhaps there could be AppListView? -public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCallbacks { +public class SwapSuccessView extends SwapView { private static final String TAG = "SwapAppsView"; public SwapSuccessView(Context context) { @@ -78,7 +72,7 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal super(context, attrs, defStyleAttr, defStyleRes); } - private Repo repo; + private Repository repo; private AppListAdapter adapter; @Override @@ -86,18 +80,64 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal super.onFinishInflate(); repo = getActivity().getSwapService().getPeerRepo(); - adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query( - AppProvider.getRepoUri(repo), AppMetadataTable.Cols.ALL, null, null, null)); - ListView listView = findViewById(R.id.list); + adapter = new AppListAdapter(); + RecyclerView listView = findViewById(R.id.list); listView.setAdapter(adapter); - // either reconnect with an existing loader or start a new one - getActivity().getSupportLoaderManager().initLoader(R.layout.swap_success, null, this); + getActivity().getSwapService().getIndex().observe(getActivity(), this::onIndexReceived); LocalBroadcastManager.getInstance(getActivity()).registerReceiver( pollForUpdatesReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); } + private void onIndexReceived(IndexV1 indexV1) { + List apps = new ArrayList<>(indexV1.getApps().size()); + HashMap apks = new HashMap<>(indexV1.getApps().size()); + CompatibilityChecker checker = new CompatibilityChecker(getContext()); + for (AppV1 a : indexV1.getApps()) { + App app = new App(); + app.name = a.getName(); + app.packageName = a.getPackageName(); + app.iconUrl = "icons/" + a.getIcon(); + try { + PackageInfo packageInfo = getContext().getPackageManager().getPackageInfo(app.packageName, 0); + app.installedVersionCode = packageInfo.versionCode; + } catch (PackageManager.NameNotFoundException ignored) { + } + Apk apk = new Apk(); + List packages = indexV1.getPackages().get(app.packageName); + if (packages != null && packages.get(0) != null) { + PackageV1 packageV1 = packages.get(0); + if (packageV1.getVersionCode() != null) { + app.autoInstallVersionCode = packageV1.getVersionCode().intValue(); + } + if (packageV1.getVersionCode() != null) { + apk.versionCode = packageV1.getVersionCode(); + } + apk.versionName = packageV1.getVersionName(); + apk.apkName = packageV1.getApkName(); + apk.hashType = packageV1.getHashType(); + apk.hash = packageV1.getHash(); + ArrayList permissions = + new ArrayList<>(packageV1.getUsesPermission().size()); + for (PermissionV1 perm : packageV1.getUsesPermission()) { + permissions.add(perm.getName()); + } + apk.requestedPermissions = permissions.toArray(new String[0]); + } + + apk.repoId = Long.MAX_VALUE; + apk.packageName = app.packageName; + apk.repoAddress = repo.getAddress(); + apk.setCompatibility(checker); + app.compatible = apk.compatible; + + apps.add(app); + apks.put(app.packageName, apk); + } + adapter.setApps(apps, apks); + } + /** * Remove relevant listeners/receivers/etc so that they do not receive and process events * when this view is not in use. @@ -109,30 +149,12 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(pollForUpdatesReceiver); } - @NonNull - @Override - public CursorLoader onCreateLoader(int id, Bundle args) { - Uri uri = TextUtils.isEmpty(currentFilterString) - ? AppProvider.getRepoUri(repo) - : AppProvider.getSearchUri(repo, currentFilterString); + private class AppListAdapter extends RecyclerView.Adapter { - return new CursorLoader(getActivity(), uri, AppMetadataTable.Cols.ALL, - null, null, AppMetadataTable.Cols.NAME); - } + private final List apps = new ArrayList<>(); + private final Map apks = new HashMap<>(); - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - adapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - adapter.swapCursor(null); - } - - private class AppListAdapter extends CursorAdapter { - - private class ViewHolder { + private class ViewHolder extends RecyclerView.ViewHolder { private final LocalBroadcastManager localBroadcastManager; @@ -177,6 +199,7 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal statusInstalled.setVisibility(View.VISIBLE); btnInstall.setVisibility(View.GONE); break; + case DownloaderService.ACTION_CONNECTION_FAILED: case DownloaderService.ACTION_INTERRUPTED: localBroadcastManager.unregisterReceiver(this); if (intent.hasExtra(DownloaderService.EXTRA_ERROR_MESSAGE)) { @@ -195,34 +218,21 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal } } - private final ContentObserver appObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - AppCompatActivity activity = getActivity(); - if (activity != null && app != null) { - app = AppProvider.Helper.findSpecificApp( - activity.getContentResolver(), - app.packageName, - app.repoId, - AppMetadataTable.Cols.ALL); - resetView(); - } - } - }; - - ViewHolder() { + ViewHolder(View view) { + super(view); localBroadcastManager = LocalBroadcastManager.getInstance(getContext()); + progressView = (ProgressBar) view.findViewById(R.id.progress); + nameView = (TextView) view.findViewById(R.id.name); + iconView = (ImageView) view.findViewById(android.R.id.icon); + btnInstall = (Button) view.findViewById(R.id.btn_install); + statusInstalled = (TextView) view.findViewById(R.id.status_installed); + statusIncompatible = (TextView) view.findViewById(R.id.status_incompatible); } public void setApp(@NonNull App app) { if (this.app == null || !this.app.packageName.equals(app.packageName)) { this.app = app; - - List availableApks = ApkProvider.Helper.findAppVersionsByRepo(getActivity(), app, repo); - if (availableApks.size() > 0) { - // Swap repos only add one version of an app, so we will just ask for the first apk. - this.apk = availableApks.get(0); - } + this.apk = apks.get(this.app.packageName); if (apk != null) { localBroadcastManager.registerReceiver(new DownloadReceiver(), @@ -268,15 +278,6 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal } }, Installer.getInstallIntentFilter(apk.getCanonicalUrl())); } - - // NOTE: Instead of continually unregistering and re-registering the observer - // (with a different URI), this could equally be done by only having one - // registration in the constructor, and using the ContentObserver.onChange(boolean, URI) - // method and inspecting the URI to see if it matches. However, this was only - // implemented on API-16, so leaving like this for now. - getActivity().getContentResolver().unregisterContentObserver(appObserver); - getActivity().getContentResolver().registerContentObserver( - AppProvider.getSpecificAppUri(this.app.packageName, this.app.repoId), true, appObserver); } resetView(); } @@ -301,11 +302,9 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal }; private void resetView() { - if (app == null) { return; } - progressView.setVisibility(View.GONE); progressView.setIndeterminate(true); @@ -313,7 +312,9 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal nameView.setText(app.name); } - app.loadWithGlide(iconView.getContext()) + String path = app.getIconPath(getContext()); + Glide.with(iconView.getContext()) + .load(App.getDownloadRequest(repo, path)) .apply(Utils.getAlwaysShowIconRequestOptions()) .into(iconView); @@ -357,44 +358,30 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal } } - @Nullable - private LayoutInflater inflater; - - AppListAdapter(@NonNull Context context, @Nullable Cursor c) { - super(context, c, FLAG_REGISTER_CONTENT_OBSERVER); - } - @NonNull - private LayoutInflater getInflater(Context context) { - if (inflater == null) { - inflater = ContextCompat.getSystemService(context, LayoutInflater.class); - } - return inflater; + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.swap_app_list_item, parent, false); + return new ViewHolder(view); } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false); - - ViewHolder holder = new ViewHolder(); - - holder.progressView = (ProgressBar) view.findViewById(R.id.progress); - holder.nameView = (TextView) view.findViewById(R.id.name); - holder.iconView = (ImageView) view.findViewById(android.R.id.icon); - holder.btnInstall = (Button) view.findViewById(R.id.btn_install); - holder.statusInstalled = (TextView) view.findViewById(R.id.status_installed); - holder.statusIncompatible = (TextView) view.findViewById(R.id.status_incompatible); - - view.setTag(holder); - bindView(view, context, cursor); - return view; + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.setApp(apps.get(position)); } @Override - public void bindView(final View view, final Context context, final Cursor cursor) { - ViewHolder holder = (ViewHolder) view.getTag(); - final App app = new App(cursor); - holder.setApp(app); + public int getItemCount() { + return apps.size(); + } + + void setApps(List apps, Map apks) { + this.apps.clear(); + this.apps.addAll(apps); + this.apks.clear(); + this.apks.putAll(apks); + notifyDataSetChanged(); } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index ff0881761..b8500278a 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -156,12 +156,18 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override public void onServiceConnected(ComponentName className, IBinder binder) { service = ((SwapService.Binder) binder).getService(); + service.getIndex().observe(SwapWorkflowActivity.this, index -> + onRepoUpdateSuccess()); + service.getIndexError().observe(SwapWorkflowActivity.this, e -> + onRepoUpdateError(e)); showRelevantView(); } @Override public void onServiceDisconnected(ComponentName className) { finish(); + service.getIndex().removeObservers(SwapWorkflowActivity.this); + service.getIndexError().removeObservers(SwapWorkflowActivity.this); service = null; } }; @@ -273,8 +279,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { backstack.clear(); localBroadcastManager = LocalBroadcastManager.getInstance(this); - localBroadcastManager.registerReceiver(downloaderInterruptedReceiver, - new IntentFilter(DownloaderService.ACTION_INTERRUPTED)); wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class); wifiApControl = WifiApControl.getInstance(this); @@ -287,7 +291,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override protected void onDestroy() { compositeDisposable.dispose(); - localBroadcastManager.unregisterReceiver(downloaderInterruptedReceiver); unbindService(serviceConnection); super.onDestroy(); } @@ -387,15 +390,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { return true; } currentView.setCurrentFilterString(newFilter); - if (currentView instanceof SelectAppsView) { - getSupportLoaderManager().restartLoader(currentView.getLayoutResId(), null, - (SelectAppsView) currentView); - } else if (currentView instanceof SwapSuccessView) { - getSupportLoaderManager().restartLoader(currentView.getLayoutResId(), null, - (SwapSuccessView) currentView); - } else { - throw new IllegalStateException(currentView.getClass() + " does not have Loader!"); - } return true; } @@ -413,8 +407,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { localBroadcastManager.registerReceiver(onWifiStateChanged, new IntentFilter(WifiStateChangeService.BROADCAST)); localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS)); - localBroadcastManager.registerReceiver(repoUpdateReceiver, - new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); localBroadcastManager.registerReceiver(bonjourFound, new IntentFilter(BonjourManager.ACTION_FOUND)); localBroadcastManager.registerReceiver(bonjourRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED)); localBroadcastManager.registerReceiver(bonjourStatusReceiver, new IntentFilter(BonjourManager.ACTION_STATUS)); @@ -440,7 +432,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { localBroadcastManager.unregisterReceiver(onWifiStateChanged); localBroadcastManager.unregisterReceiver(localRepoStatus); - localBroadcastManager.unregisterReceiver(repoUpdateReceiver); localBroadcastManager.unregisterReceiver(bonjourFound); localBroadcastManager.unregisterReceiver(bonjourRemoved); localBroadcastManager.unregisterReceiver(bonjourStatusReceiver); @@ -1451,65 +1442,28 @@ public class SwapWorkflowActivity extends AppCompatActivity { } }; - /** - * Listens for feedback about a repo update process taking place. - * Tracks an index.jar download and show the progress messages - */ - private final BroadcastReceiver repoUpdateReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String message = intent.getStringExtra(UpdateService.EXTRA_MESSAGE); - if (message == null) { - CharSequence[] repoErrors = intent.getCharSequenceArrayExtra(UpdateService.EXTRA_REPO_ERRORS); - if (repoErrors != null) { - StringBuilder msgBuilder = new StringBuilder(); - for (CharSequence error : repoErrors) { - if (msgBuilder.length() > 0) { - msgBuilder.append(" + "); - } - msgBuilder.append(error); - } - message = msgBuilder.toString(); - } - } - setUpConnectingProgressText(message); - - ProgressBar progressBar = container.findViewById(R.id.progress_bar); - Button tryAgainButton = container.findViewById(R.id.try_again); - if (progressBar == null || tryAgainButton == null) { - return; - } - - int status = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1); - if (status == UpdateService.STATUS_ERROR_GLOBAL || - status == UpdateService.STATUS_ERROR_LOCAL || - status == UpdateService.STATUS_ERROR_LOCAL_SMALL) { - progressBar.setVisibility(View.GONE); - tryAgainButton.setVisibility(View.VISIBLE); - getSwapService().removeCurrentPeerFromActive(); - return; - } else { - progressBar.setVisibility(View.VISIBLE); - tryAgainButton.setVisibility(View.GONE); - getSwapService().addCurrentPeerToActive(); - } - - if (status == UpdateService.STATUS_COMPLETE_AND_SAME - || status == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { - inflateSwapView(R.layout.swap_success); - } + private void onRepoUpdateSuccess() { + ProgressBar progressBar = container.findViewById(R.id.progress_bar); + Button tryAgainButton = container.findViewById(R.id.try_again); + if (progressBar != null && tryAgainButton != null) { + progressBar.setVisibility(View.VISIBLE); + tryAgainButton.setVisibility(View.GONE); } - }; + getSwapService().addCurrentPeerToActive(); + inflateSwapView(R.layout.swap_success); + } - private final BroadcastReceiver downloaderInterruptedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Repo repo = RepoProvider.Helper.findByUrl(context, intent.getData(), null); - if (repo != null && repo.isSwap) { - setUpConnectingProgressText(intent.getStringExtra(DownloaderService.EXTRA_ERROR_MESSAGE)); - } + private void onRepoUpdateError(Exception e) { + ProgressBar progressBar = container.findViewById(R.id.progress_bar); + Button tryAgainButton = container.findViewById(R.id.try_again); + if (progressBar != null && tryAgainButton != null) { + progressBar.setVisibility(View.GONE); + tryAgainButton.setVisibility(View.VISIBLE); } - }; + String msg = e.getMessage() == null ? "Error updating repo " + e : e.getMessage(); + setUpConnectingProgressText(msg); + getSwapService().removeCurrentPeerFromActive(); + } private void setUpConnectingView() { TextView heading = container.findViewById(R.id.progress_text); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index 31c812ed2..071ad6a2b 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -11,12 +11,13 @@ import android.text.TextUtils; import android.util.Log; import org.apache.commons.net.util.SubnetUtils; +import org.fdroid.database.Repository; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; import java.net.Inet6Address; import java.net.InetAddress; @@ -202,16 +203,20 @@ public class WifiStateChangeService extends Worker { } else { scheme = "http"; } - Repo repo = new Repo(); - repo.name = Preferences.get().getLocalRepoName(); - repo.address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo", + Context context = WifiStateChangeService.this.getApplicationContext(); + String address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo", scheme, FDroidApp.ipAddressString, FDroidApp.port); + // the fingerprint for the local repo's signing key + LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context); + Certificate localCert = localRepoKeyStore.getCertificate(); + String cert = localCert == null ? + null : Hasher.hex(localCert).toLowerCase(Locale.US); + Repository repo = FDroidApp.createSwapRepo(address, cert); if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver return; } - Context context = WifiStateChangeService.this.getApplicationContext(); LocalRepoManager lrm = LocalRepoManager.get(context); lrm.writeIndexPage(Utils.getSharingUri(FDroidApp.repo).toString()); @@ -219,11 +224,6 @@ public class WifiStateChangeService extends Worker { return; } - // the fingerprint for the local repo's signing key - LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context); - Certificate localCert = localRepoKeyStore.getCertificate(); - repo.fingerprint = Utils.calcFingerprint(localCert); - FDroidApp.repo = repo; /* diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/peers/BonjourPeer.java b/app/src/full/java/org/fdroid/fdroid/nearby/peers/BonjourPeer.java index 6c5616ca6..4308d1a41 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/peers/BonjourPeer.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/peers/BonjourPeer.java @@ -30,7 +30,7 @@ public class BonjourPeer extends WifiPeer { String type = serviceInfo.getPropertyString(TYPE); String fingerprint = serviceInfo.getPropertyString(FINGERPRINT); if (type == null || !type.startsWith("fdroidrepo") - || TextUtils.equals(FDroidApp.repo.fingerprint, fingerprint)) { + || TextUtils.equals(FDroidApp.repo.getFingerprint(), fingerprint)) { return null; } return new BonjourPeer(serviceInfo); diff --git a/app/src/full/res/layout/swap_success.xml b/app/src/full/res/layout/swap_success.xml index 2c6cd1647..c03ef222d 100644 --- a/app/src/full/res/layout/swap_success.xml +++ b/app/src/full/res/layout/swap_success.xml @@ -9,10 +9,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - \ No newline at end of file diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 3e4b17937..303f726fb 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -59,7 +59,6 @@ import org.fdroid.fdroid.Preferences.Theme; import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.DBHelper; -import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.ApkFileProvider; import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.nearby.PublicSourceDirProvider; @@ -68,6 +67,7 @@ import org.fdroid.fdroid.nearby.WifiStateChangeService; import org.fdroid.fdroid.net.ConnectivityMonitorService; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.work.CleanCacheWorker; +import org.fdroid.index.IndexFormatVersion; import java.io.IOException; import java.nio.ByteBuffer; @@ -105,7 +105,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio public static volatile SubnetUtils.SubnetInfo subnetInfo; public static volatile String ssid; public static volatile String bssid; - public static volatile Repo repo = new Repo(); + public static volatile Repository repo; public static volatile List repos; @@ -230,7 +230,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio subnetInfo = UNSET_SUBNET_INFO; ssid = ""; bssid = ""; - repo = new Repo(); + repo = null; } @Override @@ -561,6 +561,11 @@ public class FDroidApp extends Application implements androidx.work.Configuratio return null; } + public static Repository createSwapRepo(String address, String certificate) { + long now = System.currentTimeMillis(); + return new Repository(42L, address, now, IndexFormatVersion.ONE, certificate, 20001L, 42, now); + } + public static Context getInstance() { return instance; } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 1781f602a..923cb9f01 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -21,6 +21,7 @@ package org.fdroid.fdroid; import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -361,26 +362,8 @@ public final class Utils { return b.build(); } - @Deprecated - public static Uri getSharingUri(Repo repo) { - if (TextUtils.isEmpty(repo.address)) { - return Uri.parse("http://wifi-not-enabled"); - } - Uri localRepoUri = getLocalRepoUri(repo); - Uri.Builder b = localRepoUri.buildUpon(); - b.scheme(localRepoUri.getScheme().replaceFirst("http", "fdroidrepo")); - b.appendQueryParameter("swap", "1"); - if (!TextUtils.isEmpty(FDroidApp.bssid)) { - b.appendQueryParameter("bssid", FDroidApp.bssid); - if (!TextUtils.isEmpty(FDroidApp.ssid)) { - b.appendQueryParameter("ssid", FDroidApp.ssid); - } - } - return b.build(); - } - public static Uri getSharingUri(Repository repo) { - if (TextUtils.isEmpty(repo.getAddress())) { + if (repo == null || TextUtils.isEmpty(repo.getAddress())) { return Uri.parse("http://wifi-not-enabled"); } Uri localRepoUri = getLocalRepoUri(repo); @@ -803,6 +786,18 @@ public final class Utils { return versionName; } + public static String getApplicationLabel(Context context, String packageName) { + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + return appInfo.loadLabel(pm).toString(); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { + Utils.debugLog(TAG, "Could not get application label: " + e.getMessage()); + } + return packageName; // all else fails, return packageName + } + public static String getUserAgent() { return "F-Droid " + BuildConfig.VERSION_NAME; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index eacb23993..30db61b72 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -75,7 +75,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable { // these are never set by the Apk/package index metadata @JsonIgnore - protected String repoAddress; + public String repoAddress; @JsonIgnore long repoVersion; @JsonIgnore diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 98e4dfa7c..8fc6062ea 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -264,7 +264,7 @@ public class App extends ValueObject implements Comparable, Parcelable { * URL to download the app's icon. (Set only from localized block, see also * {@link #iconFromApk} and {@link #getIconPath(Context)} (Context)} */ - private String iconUrl; + public String iconUrl; public static String getIconName(String packageName, long versionCode) { return packageName + "_" + versionCode + ".png"; @@ -934,7 +934,7 @@ public class App extends ValueObject implements Comparable, Parcelable { return new DownloadRequest(path, mirrors, NetCipher.getProxy(), null, null); } - private String getIconPath(Context context) { + public String getIconPath(Context context) { String path; if (TextUtils.isEmpty(iconUrl)) { if (TextUtils.isEmpty(iconFromApk)) { diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index 48912fb46..38b9b758b 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -255,7 +255,13 @@ public class DownloaderService extends Service { // right after the app gets re-recreated downloads get re-triggered, so repo can still be null FDroidDatabase db = DBHelper.getDb(getApplicationContext()); repo = db.getRepositoryDao().getRepository(repoId); - if (repo == null) return; // repo might have been deleted in the meantime + if (repo == null) { + String canonical = canonicalUrl.toString(); + if (canonical.startsWith("http://1") && canonical.contains(":8888/")) { + String address = canonical.split(":8888/")[0] + ":8888/"; + repo = FDroidApp.createSwapRepo(address, null); // fake repo for swap + } else return; // repo might have been deleted in the meantime + } } downloader = DownloaderFactory.INSTANCE.create(repo, canonicalUrl, localFile); downloader.setListener(new ProgressListener() { diff --git a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java index e6abe538a..5cf7bb7ad 100644 --- a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java +++ b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java @@ -123,8 +123,8 @@ public class SwapRepoTest { FDroidApp.initWifiSettings(); FDroidApp.ipAddressString = "127.0.0.1"; FDroidApp.subnetInfo = new SubnetUtils("127.0.0.0/8").getInfo(); - FDroidApp.repo.name = "test"; - FDroidApp.repo.address = "http://" + FDroidApp.ipAddressString + ":" + FDroidApp.port + "/fdroid/repo"; + String address = "http://" + FDroidApp.ipAddressString + ":" + FDroidApp.port + "/fdroid/repo"; + FDroidApp.repo = FDroidApp.createSwapRepo(address, null); LocalRepoService.runProcess(context, new String[]{context.getPackageName()}); File indexJarFile = LocalRepoManager.get(context).getIndexJar(); @@ -147,7 +147,7 @@ public class SwapRepoTest { assertFalse(TextUtils.isEmpty(signingCert)); assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert))); - Repo repo = MultiIndexUpdaterTest.createRepo(FDroidApp.repo.name, FDroidApp.repo.address, + Repo repo = MultiIndexUpdaterTest.createRepo("", FDroidApp.repo.getAddress(), context, signingCert); IndexUpdater updater = new IndexUpdater(context, repo); updater.update(); @@ -177,11 +177,4 @@ public class SwapRepoTest { } } } - - class TestLocalRepoService extends LocalRepoService { - @Override - protected void onHandleIntent(Intent intent) { - super.onHandleIntent(intent); - } - } } \ No newline at end of file