mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-15 01:23:56 -05:00
[app] Adapt nearby/swap to new DB
This commit is contained in:
committed by
Hans-Christoph Steiner
parent
48d646361b
commit
d61ecbfa08
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user