[app] Adapt nearby/swap to new DB

This commit is contained in:
Torsten Grote
2022-05-02 17:59:39 -03:00
committed by Hans-Christoph Steiner
parent 48d646361b
commit d61ecbfa08
19 changed files with 408 additions and 706 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -181,7 +181,7 @@ public class BonjourManager {
HashMap<String, String> 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");

View File

@@ -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<String, App> apps = new ConcurrentHashMap<>();
private final List<App> 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("<li><a href=\"/fdroid/repo/")
.append(app.installedApk.apkName)
.append("\"><img width=\"32\" height=\"32\" src=\"/fdroid/repo/icons/")
.append(app.packageName)
.append("_")
.append(app.installedApk.versionCode)
.append(".apk\">")
.append("<img width=\"32\" height=\"32\" src=\"/fdroid/repo/icons/")
.append(app.packageName)
.append("_")
.append(app.installedApk.versionCode)
@@ -213,7 +204,7 @@ public final class LocalRepoManager {
private static void attemptToDelete(@NonNull File file) {
if (!file.delete()) {
Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
Log.i(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
}
}
@@ -243,10 +234,10 @@ public final class LocalRepoManager {
}
/**
* Get the {@code index.jar} file that represents the local swap repo.
* Get the {@code index-v1.jar} file that represents the local swap repo.
*/
public File getIndexJar() {
return xmlIndexJar;
return indexJar;
}
public File getWebRoot() {
@@ -257,265 +248,50 @@ public final class LocalRepoManager {
deleteContents(repoDir);
}
public void copyApksToRepo() {
copyApksToRepo(new ArrayList<>(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<String> 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<String> 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<PackageV1> 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<String, App> apps, OutputStream output)
throws IOException, LocalRepoKeyStore.InitException {
serializer.setOutput(output, "UTF-8");
serializer.startDocument(null, null);
serializer.startTag("", "fdroid");
// <repo> 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");
// <application> blocks
for (Map.Entry<String, App> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Cursor> {
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<PackageInfo> 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<Cursor> 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<Cursor> 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<InstalledApp> allPackages;
private final List<InstalledApp> filteredPackages = new ArrayList<>();
AppListAdapter(@NonNull ListView listView, List<PackageInfo> 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));
}
}
}

View File

@@ -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());

View File

@@ -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<String> appsToSwap = new HashSet<>();
private final Set<Peer> activePeers = new HashSet<>();
private final MutableLiveData<IndexV1> index = new MutableLiveData<>();
private final MutableLiveData<Exception> 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<String, IndexV1> 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<IndexV1> getIndex() {
return index;
}
public LiveData<Exception> 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 <a href="https://gitlab.com/fdroid/fdroidclient/issues/295">TOFU in swap</a>
* @see <a href="https://gitlab.com/fdroid/fdroidclient/issues/703">
* signing key fingerprint should be sole ID for repos in the database</a>
*/
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.
*/

View File

@@ -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<Cursor> {
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<App> apps = new ArrayList<>(indexV1.getApps().size());
HashMap<String, Apk> 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<PackageV1> 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<String> 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<AppListAdapter.ViewHolder> {
return new CursorLoader(getActivity(), uri, AppMetadataTable.Cols.ALL,
null, null, AppMetadataTable.Cols.NAME);
}
private final List<App> apps = new ArrayList<>();
private final Map<String, Apk> apks = new HashMap<>();
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> 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<Apk> 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<App> apps, Map<String, Apk> apks) {
this.apps.clear();
this.apps.addAll(apps);
this.apks.clear();
this.apks.putAll(apks);
notifyDataSetChanged();
}
}

View File

@@ -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);

View File

@@ -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;
/*

View File

@@ -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);

View File

@@ -9,10 +9,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
swap:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"/>
</org.fdroid.fdroid.nearby.SwapSuccessView>

View File

@@ -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<Repository> 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;
}

View File

@@ -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;
}

View File

@@ -75,7 +75,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
// these are never set by the Apk/package index metadata
@JsonIgnore
protected String repoAddress;
public String repoAddress;
@JsonIgnore
long repoVersion;
@JsonIgnore

View File

@@ -264,7 +264,7 @@ public class App extends ValueObject implements Comparable<App>, 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<App>, 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)) {

View File

@@ -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() {

View File

@@ -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);
}
}
}