diff --git a/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java new file mode 100644 index 000000000..390763942 --- /dev/null +++ b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java @@ -0,0 +1,199 @@ +package org.fdroid.fdroid.updater; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.os.Looper; +import android.support.test.InstrumentationRegistry; +import android.text.TextUtils; +import android.util.Log; +import org.fdroid.fdroid.BuildConfig; +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.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.localrepo.LocalRepoKeyStore; +import org.fdroid.fdroid.localrepo.LocalRepoManager; +import org.fdroid.fdroid.localrepo.LocalRepoService; +import org.fdroid.fdroid.net.LocalHTTPD; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.security.cert.Certificate; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class SwapRepoEmulatorTest { + public static final String TAG = "SwapRepoEmulatorTest"; + + /** + * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run() + */ + @Test + public void testSwap() + throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException { + Looper.prepare(); + LocalHTTPD localHttpd = null; + try { + Log.i(TAG, "REPO: " + FDroidApp.repo); + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + Preferences.setupForTests(context); + + FDroidApp.initWifiSettings(); + assertNull(FDroidApp.repo.address); + + final CountDownLatch latch = new CountDownLatch(1); + new Thread() { + @Override + public void run() { + while (FDroidApp.repo.address == null) { + try { + Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address); + Thread.sleep(1000); + } catch (InterruptedException e) { + // ignored + } + } + latch.countDown(); + } + }.start(); + latch.await(10, TimeUnit.MINUTES); + assertNotNull(FDroidApp.repo.address); + + LocalRepoService.runProcess(context, new String[]{context.getPackageName()}); + Log.i(TAG, "REPO: " + FDroidApp.repo); + File indexJarFile = LocalRepoManager.get(context).getIndexJar(); + assertTrue(indexJarFile.isFile()); + + localHttpd = new LocalHTTPD( + context, + null, + FDroidApp.port, + LocalRepoManager.get(context).getWebRoot(), + false); + localHttpd.start(); + Thread.sleep(100); // give the server some tine to start. + assertTrue(localHttpd.isAlive()); + + LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context); + Certificate localCert = localRepoKeyStore.getCertificate(); + String signingCert = Hasher.hex(localCert); + assertFalse(TextUtils.isEmpty(signingCert)); + assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert))); + + Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + 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); + } + + 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.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); + assertTrue(repo.isSwap); + assertNotEquals(-1, repo.getId()); + assertTrue(repo.name.startsWith(FDroidApp.repo.name)); + assertEquals(lastEtag, repo.lastetag); + assertNull(repo.lastUpdated); + + assertTrue(isPortInUse(FDroidApp.ipAddressString, FDroidApp.port)); + Thread.sleep(100); + IndexUpdater updater = new IndexUpdater(context, repo); + updater.update(); + assertTrue(updater.hasChanged()); + + repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address); + final Date lastUpdated = repo.lastUpdated; + assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0); + + App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), + context.getPackageName(), repo.getId()); + assertEquals(context.getPackageName(), app.packageName); + + List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); + assertEquals(1, apks.size()); + for (Apk apk : apks) { + Log.i(TAG, "Apk: " + apk); + assertEquals(context.getPackageName(), apk.packageName); + assertEquals(BuildConfig.VERSION_NAME, apk.versionName); + assertEquals(BuildConfig.VERSION_CODE, apk.versionCode); + assertEquals(app.repoId, apk.repoId); + } + + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0); + HashSet<String> packageNames = new HashSet<>(); + for (ResolveInfo resolveInfo : resolveInfoList) { + if (!isSystemPackage(resolveInfo)) { + Log.i(TAG, "resolveInfo: " + resolveInfo); + packageNames.add(resolveInfo.activityInfo.packageName); + } + } + LocalRepoService.runProcess(context, packageNames.toArray(new String[0])); + + updater = new IndexUpdater(context, repo); + updater.update(); + assertTrue(updater.hasChanged()); + assertTrue("repo lastUpdated should be updated", lastUpdated.compareTo(repo.lastUpdated) < 0); + + for (String packageName : packageNames) { + assertNotNull(ApkProvider.Helper.findByPackageName(context, packageName)); + } + } finally { + if (localHttpd != null) { + localHttpd.stop(); + } + } + if (localHttpd != null) { + assertFalse(localHttpd.isAlive()); + } + } + + private boolean isPortInUse(String host, int port) { + boolean result = false; + + try { + (new Socket(host, port)).close(); + result = true; + } catch (IOException e) { + // Could not connect. + e.printStackTrace(); + } + return result; + } + + private boolean isSystemPackage(ResolveInfo resolveInfo) { + return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } +} diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index 70c2bbe0f..b4cfe4e2f 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -80,6 +80,9 @@ <service android:name=".localrepo.CacheSwapAppsService" android:exported="false"/> + <service + android:name=".localrepo.LocalRepoService" + android:exported="false"/> <service android:name=".localrepo.TreeUriScannerIntentService" android:exported="false"/> diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java index 1f818c7fb..12ee3f23d 100644 --- a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -21,6 +21,8 @@ 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; @@ -246,6 +248,10 @@ public final class LocalRepoManager { return xmlIndexJar; } + public File getWebRoot() { + return webRoot; + } + public void deleteRepo() { deleteContents(repoDir); } diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java new file mode 100644 index 000000000..554af9c9a --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java @@ -0,0 +1,163 @@ +package org.fdroid.fdroid.localrepo; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.EXTRA_TYPE; +import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_COMPLETE; +import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_ERROR; +import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_STATUS; + +/** + * Handles setting up and generating the local repo used to swap apps, including + * the {@code index.jar}, the symlinks to the shared APKs, etc. + * <p/> + * The work is done in a {@link Thread} so that new incoming {@code Intents} + * are not blocked by processing. A new {@code Intent} immediately nullifies + * the current state because it means the user has chosen a different set of + * apps. That is also enforced here since new {@code Intent}s with the same + * {@link Set} of apps as the current one are ignored. Having the + * {@code Thread} also makes it easy to kill work that is in progress. + */ +public class LocalRepoService extends IntentService { + public static final String TAG = "LocalRepoService"; + + public static final String ACTION_PROGRESS = "org.fdroid.fdroid.localrepo.LocalRepoService.action.PROGRESS"; + public static final String ACTION_COMPLETE = "org.fdroid.fdroid.localrepo.LocalRepoService.action.COMPLETE"; + public static final String ACTION_ERROR = "org.fdroid.fdroid.localrepo.LocalRepoService.action.ERROR"; + + public static final String EXTRA_MESSAGE = "org.fdroid.fdroid.localrepo.LocalRepoService.extra.MESSAGE"; + + public static final String ACTION_CREATE = "org.fdroid.fdroid.localrepo.action.CREATE"; + public static final String EXTRA_PACKAGE_NAMES = "org.fdroid.fdroid.localrepo.extra.PACKAGE_NAMES"; + + private final HashSet<String> selectedApps = new HashSet<>(); + + private GenerateLocalRepoThread thread; + + public LocalRepoService() { + super("LocalRepoService"); + } + + /** + * Creates a skeleton swap repo with only F-Droid itself in it + */ + public static void create(Context context) { + create(context, Collections.singleton(context.getPackageName())); + } + + /** + * Sets up the local repo with the included {@code packageNames} + */ + public static void create(Context context, Set<String> packageNames) { + Intent intent = new Intent(context, LocalRepoService.class); + intent.setAction(ACTION_CREATE); + intent.putExtra(EXTRA_PACKAGE_NAMES, packageNames.toArray(new String[0])); + context.startService(intent); + } + + @Override + protected void onHandleIntent(Intent intent) { + String[] packageNames = intent.getStringArrayExtra(EXTRA_PACKAGE_NAMES); + if (packageNames == null || packageNames.length == 0) { + Utils.debugLog(TAG, "no packageNames found, quiting"); + return; + } + + boolean changed = Collections.addAll(selectedApps, packageNames); + if (!changed) { + Utils.debugLog(TAG, "packageNames list unchanged, quiting"); + return; + } + + if (thread != null) { + thread.interrupt(); + } + thread = new GenerateLocalRepoThread(); + thread.start(); + } + + private class GenerateLocalRepoThread extends Thread { + private static final String TAG = "GenerateLocalRepoThread"; + + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); + runProcess(LocalRepoService.this, selectedApps); + } + } + + public static void runProcess(Context context, Set<String> selectedApps) { + try { + final LocalRepoManager lrm = LocalRepoManager.get(context); + broadcast(context, ACTION_PROGRESS, R.string.deleting_repo); + lrm.deleteRepo(); + for (String app : selectedApps) { + broadcast(context, ACTION_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, ACTION_PROGRESS, R.string.writing_index_jar); + lrm.writeIndexJar(); + broadcast(context, ACTION_PROGRESS, R.string.linking_apks); + lrm.copyApksToRepo(); + broadcast(context, ACTION_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(); + + broadcast(context, ACTION_COMPLETE, null); + } catch (IOException | XmlPullParserException | LocalRepoKeyStore.InitException e) { + broadcast(context, ACTION_ERROR, e.getLocalizedMessage()); + e.printStackTrace(); + } + } + + /** + * Translate Android style broadcast {@link Intent}s to {@code PrepareSwapRepo} + */ + static void broadcast(Context context, String action, String message) { + Intent intent = new Intent(context, SwapWorkflowActivity.class); + intent.setAction(SwapWorkflowActivity.PrepareSwapRepo.ACTION); + switch (action) { + case ACTION_PROGRESS: + intent.putExtra(EXTRA_TYPE, TYPE_STATUS); + break; + case ACTION_COMPLETE: + intent.putExtra(EXTRA_TYPE, TYPE_COMPLETE); + break; + case ACTION_ERROR: + intent.putExtra(EXTRA_TYPE, TYPE_ERROR); + break; + default: + throw new IllegalArgumentException("unsupported action"); + } + if (message != null) { + Utils.debugLog(TAG, "Preparing swap: " + message); + intent.putExtra(EXTRA_MESSAGE, message); + } + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + + static void broadcast(Context context, String action, int resId) { + broadcast(context, action, context.getString(resId)); + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index ee92c4dca..d40a35ae1 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -12,7 +12,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.net.wifi.WifiManager; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -51,19 +50,16 @@ import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; -import org.fdroid.fdroid.localrepo.LocalRepoManager; +import org.fdroid.fdroid.localrepo.LocalRepoService; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.SwapView; import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.HttpDownloader; -import java.util.Arrays; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -103,7 +99,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { private Toolbar toolbar; private SwapView currentView; private boolean hasPreparedLocalRepo; - private PrepareSwapRepo updateSwappableAppsTask; private NewRepoConfig confirmSwapConfig; private LocalBroadcastManager localBroadcastManager; private WifiManager wifiManager; @@ -504,12 +499,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { // as we are starting over now. getService().swapWith(null); - if (!getService().isEnabled()) { - if (!LocalRepoManager.get(this).getIndexJar().exists()) { - Utils.debugLog(TAG, "Preparing initial repo with only F-Droid, until we have allowed the user to configure their own repo."); - new PrepareInitialSwapRepo().execute(); - } - } + LocalRepoService.create(this); inflateSwapView(R.layout.swap_start_swap); } @@ -591,30 +581,34 @@ public class SwapWorkflowActivity extends AppCompatActivity { ((FDroidApp) getApplication()).sendViaBluetooth(this, Activity.RESULT_OK, BuildConfig.APPLICATION_ID); } - // TODO: Figure out whether they have changed since last time UpdateAsyncTask was run. - // If the local repo is running, then we can ask it what apps it is swapping and compare with that. - // Otherwise, probably will need to scan the file system. + /** + * TODO: Figure out whether they have changed since last time LocalRepoService + * was run. If the local repo is running, then we can ask it what apps it is + * swapping and compare with that. Otherwise, probably will need to scan the + * file system. + */ public void onAppsSelected() { - if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { - updateSwappableAppsTask = new PrepareSwapRepo(getService().getAppsToSwap()); - updateSwappableAppsTask.execute(); + if (hasPreparedLocalRepo) { + onLocalRepoPrepared(); + } else { + LocalRepoService.create(this, getService().getAppsToSwap()); getService().setCurrentView(R.layout.swap_connecting); inflateSwapView(R.layout.swap_connecting); - } else { - onLocalRepoPrepared(); } } /** * Once the UpdateAsyncTask has finished preparing our repository index, we can * show the next screen to the user. This will be one of two things: - * * If we directly selected a peer to swap with initially, we will skip straight to getting - * the list of apps from that device. - * * Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code", - * then we want to show a QR code or NFC dialog. + * <ol> + * <li>If we directly selected a peer to swap with initially, we will skip straight to getting + * the list of apps from that device.</li> + * <li>Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code", + * then we want to show a QR code or NFC dialog.</li> + * </ol> */ public void onLocalRepoPrepared() { - updateSwappableAppsTask = null; + // TODO ditch this, use a message from LocalRepoService. Maybe? hasPreparedLocalRepo = true; if (getService().isConnectingWithPeer()) { startSwappingWithPeer(); @@ -637,6 +631,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { // during the wifi qr code being shown too. boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo)); + // TODO move all swap-specific preferences to a SharedPreferences instance for SwapWorkflowActivity if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) { inflateSwapView(R.layout.swap_nfc); return true; @@ -779,82 +774,13 @@ public class SwapWorkflowActivity extends AppCompatActivity { service.getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService } - class PrepareInitialSwapRepo extends PrepareSwapRepo { - PrepareInitialSwapRepo() { - super(new HashSet<>(Arrays.asList(new String[]{BuildConfig.APPLICATION_ID}))); - } - } - - class PrepareSwapRepo extends AsyncTask<Void, Void, Void> { - + public class PrepareSwapRepo { public static final String ACTION = "PrepareSwapRepo.Action"; public static final String EXTRA_MESSAGE = "PrepareSwapRepo.Status.Message"; public static final String EXTRA_TYPE = "PrepareSwapRepo.Action.Type"; public static final int TYPE_STATUS = 0; public static final int TYPE_COMPLETE = 1; public static final int TYPE_ERROR = 2; - - @NonNull - protected final Set<String> selectedApps; - - @NonNull - protected final Uri sharingUri; - - @NonNull - protected final Context context; - - PrepareSwapRepo(@NonNull Set<String> apps) { - context = SwapWorkflowActivity.this; - selectedApps = apps; - sharingUri = Utils.getSharingUri(FDroidApp.repo); - } - - private void broadcast(int type) { - broadcast(type, null); - } - - private void broadcast(int type, String message) { - Intent intent = new Intent(ACTION); - intent.putExtra(EXTRA_TYPE, type); - if (message != null) { - Utils.debugLog(TAG, "Preparing swap: " + message); - intent.putExtra(EXTRA_MESSAGE, message); - } - LocalBroadcastManager.getInstance(SwapWorkflowActivity.this).sendBroadcast(intent); - } - - @Override - protected Void doInBackground(Void... params) { - try { - final LocalRepoManager lrm = LocalRepoManager.get(context); - broadcast(TYPE_STATUS, getString(R.string.deleting_repo)); - lrm.deleteRepo(); - for (String app : selectedApps) { - broadcast(TYPE_STATUS, String.format(getString(R.string.adding_apks_format), app)); - lrm.addApp(context, app); - } - lrm.writeIndexPage(sharingUri.toString()); - broadcast(TYPE_STATUS, getString(R.string.writing_index_jar)); - lrm.writeIndexJar(); - broadcast(TYPE_STATUS, getString(R.string.linking_apks)); - lrm.copyApksToRepo(); - broadcast(TYPE_STATUS, getString(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_BACKGROUND); - lrm.copyIconsToRepo(); - } - }.start(); - - broadcast(TYPE_COMPLETE); - } catch (Exception e) { - broadcast(TYPE_ERROR); - Log.e(TAG, "", e); - } - return null; - } } /** diff --git a/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java new file mode 100644 index 000000000..a9534989f --- /dev/null +++ b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java @@ -0,0 +1,14 @@ +package org.fdroid.fdroid.data; + +import android.content.Context; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(App.class) +public class ShadowApp extends ValueObject { + + @Implementation + protected static int[] getMinTargetMaxSdkVersions(Context context, String packageName) { + return new int[]{10, 23, Apk.SDK_VERSION_MAX_VALUE}; + } +} diff --git a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java new file mode 100644 index 000000000..1436c5040 --- /dev/null +++ b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java @@ -0,0 +1,187 @@ +package org.fdroid.fdroid.updater; + +import android.content.ContentResolver; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import org.apache.commons.net.util.SubnetUtils; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Hasher; +import org.fdroid.fdroid.IndexUpdater; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.TestUtils; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.DBHelper; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.data.ShadowApp; +import org.fdroid.fdroid.data.TempAppProvider; +import org.fdroid.fdroid.localrepo.LocalRepoKeyStore; +import org.fdroid.fdroid.localrepo.LocalRepoManager; +import org.fdroid.fdroid.localrepo.LocalRepoService; +import org.fdroid.fdroid.net.LocalHTTPD; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowLog; +import org.robolectric.shadows.ShadowPackageManager; + +import java.io.File; +import java.io.IOException; +import java.security.cert.Certificate; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; + +/** + * This test almost works, it needs to have the {@link android.content.ContentProvider} + * and {@link ContentResolver} stuff worked out. It currently fails as + * {@code updater.update()}. + */ +@Ignore +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowApp.class) +public class SwapRepoTest { + + private LocalHTTPD localHttpd; + + protected ShadowContentResolver shadowContentResolver; + protected ContentResolver contentResolver; + protected ContextWrapper context; + + @Before + public void setUp() { + ShadowLog.stream = System.out; + + contentResolver = RuntimeEnvironment.application.getContentResolver(); + shadowContentResolver = Shadows.shadowOf(contentResolver); + context = new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) { + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } + }; + + TestUtils.registerContentProvider(ApkProvider.getAuthority(), ApkProvider.class); + TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); + TestUtils.registerContentProvider(RepoProvider.getAuthority(), RepoProvider.class); + TestUtils.registerContentProvider(TempAppProvider.getAuthority(), TempAppProvider.class); + + Preferences.setupForTests(context); + } + + @After + public final void tearDownBase() { + DBHelper.clearDbHelperSingleton(); + } + + /** + * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run() + */ + @Test + public void testSwap() + throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException { + + PackageManager packageManager = context.getPackageManager(); + ShadowPackageManager shadowPackageManager = shadowOf(packageManager); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.flags = 0; + appInfo.packageName = context.getPackageName(); + appInfo.minSdkVersion = 10; + appInfo.targetSdkVersion = 23; + appInfo.sourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath(); + appInfo.publicSourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath(); + System.out.println("appInfo.sourceDir " + appInfo.sourceDir); + appInfo.name = "F-Droid"; + + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = appInfo.packageName; + packageInfo.applicationInfo = appInfo; + packageInfo.versionCode = 1002001; + packageInfo.versionName = "1.2-fake"; + shadowPackageManager.addPackage(packageInfo); + + try { + 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"; + + LocalRepoService.runProcess(context, new String[]{context.getPackageName()}); + File indexJarFile = LocalRepoManager.get(context).getIndexJar(); + System.out.println("indexJarFile:" + indexJarFile); + assertTrue(indexJarFile.isFile()); + + localHttpd = new LocalHTTPD( + context, + FDroidApp.ipAddressString, + FDroidApp.port, + LocalRepoManager.get(context).getWebRoot(), + false); + localHttpd.start(); + Thread.sleep(100); // give the server some tine to start. + assertTrue(localHttpd.isAlive()); + + LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context); + Certificate localCert = localRepoKeyStore.getCertificate(); + String signingCert = Hasher.hex(localCert); + assertFalse(TextUtils.isEmpty(signingCert)); + assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert))); + + Repo repo = MultiIndexUpdaterTest.createRepo(FDroidApp.repo.name, FDroidApp.repo.address, + context, signingCert); + IndexUpdater updater = new IndexUpdater(context, repo); + updater.update(); + assertTrue(updater.hasChanged()); + updater.processDownloadedFile(indexJarFile); + + boolean foundRepo = false; + for (Repo repoFromDb : RepoProvider.Helper.all(context)) { + if (TextUtils.equals(repo.address, repoFromDb.address)) { + foundRepo = true; + repo = repoFromDb; + } + } + assertTrue(foundRepo); + + assertNotEquals(-1, repo.getId()); + List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); + assertEquals(1, apks.size()); + for (Apk apk : apks) { + System.out.println(apk); + } + //MultiIndexUpdaterTest.assertApksExist(apks, context.getPackageName(), new int[]{BuildConfig.VERSION_CODE}); + Thread.sleep(10000); + } finally { + if (localHttpd != null) { + localHttpd.stop(); + } + } + } + + class TestLocalRepoService extends LocalRepoService { + @Override + protected void onHandleIntent(Intent intent) { + super.onHandleIntent(intent); + } + } +} \ No newline at end of file