mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-06-17 12:19:52 -04:00
Clean up app gradle modules
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class AssetUtils {
|
||||
|
||||
private static final String TAG = "Utils";
|
||||
|
||||
/**
|
||||
* This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
|
||||
*/
|
||||
@Nullable
|
||||
public static File copyAssetToDir(Context context, String assetName, File directory) {
|
||||
File tempFile = null;
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
try {
|
||||
tempFile = File.createTempFile(assetName, ".testasset", directory);
|
||||
Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory);
|
||||
input = context.getAssets().open(assetName);
|
||||
output = new FileOutputStream(tempFile);
|
||||
Utils.copy(input, output);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Check the context is from Instrumentation.getContext()");
|
||||
fail(e.getMessage());
|
||||
} finally {
|
||||
Utils.closeQuietly(output);
|
||||
Utils.closeQuietly(input);
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Runs through all of the translated strings and tests them with the same format
|
||||
* values that the source strings expect. This is to ensure that the formats in
|
||||
* the translations are correct in number and in type (e.g. {@code s} or {@code s}.
|
||||
* It reads the source formats and then builds {@code formats} to represent the
|
||||
* position and type of the formats. Then it runs through all of the translations
|
||||
* with formats of the correct number and type.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LocalizationTest {
|
||||
public static final String TAG = "LocalizationTest";
|
||||
|
||||
private final Pattern androidFormat = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)");
|
||||
private final Locale[] locales = Locale.getAvailableLocales();
|
||||
private final HashSet<String> localeNames = new HashSet<>(locales.length);
|
||||
|
||||
private AssetManager assets;
|
||||
private Configuration config;
|
||||
private Resources resources;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
for (Locale locale : Languages.LOCALES_TO_TEST) {
|
||||
localeNames.add(locale.toString());
|
||||
}
|
||||
for (Locale locale : locales) {
|
||||
localeNames.add(locale.toString());
|
||||
}
|
||||
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
Context context = instrumentation.getTargetContext();
|
||||
assets = context.getAssets();
|
||||
config = context.getResources().getConfiguration();
|
||||
config.locale = Locale.ENGLISH;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAllPlural() throws IllegalAccessException {
|
||||
Field[] fields = R.plurals.class.getDeclaredFields();
|
||||
|
||||
HashMap<String, String> haveFormats = new HashMap<>();
|
||||
for (Field field : fields) {
|
||||
//Log.i(TAG, field.getName());
|
||||
int resId = field.getInt(int.class);
|
||||
CharSequence string = resources.getQuantityText(resId, 4);
|
||||
//Log.i(TAG, field.getName() + ": '" + string + "'");
|
||||
Matcher matcher = androidFormat.matcher(string);
|
||||
int matches = 0;
|
||||
char[] formats = new char[5];
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group(0);
|
||||
char formatType = match.charAt(match.length() - 1);
|
||||
switch (match.length()) {
|
||||
case 2:
|
||||
formats[matches] = formatType;
|
||||
matches++;
|
||||
break;
|
||||
case 4:
|
||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
||||
break;
|
||||
case 5:
|
||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
||||
}
|
||||
}
|
||||
haveFormats.put(field.getName(), new String(formats).trim());
|
||||
}
|
||||
|
||||
for (Locale locale : locales) {
|
||||
config.locale = locale;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
for (Field field : fields) {
|
||||
String formats = null;
|
||||
try {
|
||||
int resId = field.getInt(int.class);
|
||||
for (int quantity = 0; quantity < 567; quantity++) {
|
||||
resources.getQuantityString(resId, quantity);
|
||||
}
|
||||
|
||||
formats = haveFormats.get(field.getName());
|
||||
switch (formats) {
|
||||
case "d":
|
||||
resources.getQuantityString(resId, 1, 1);
|
||||
break;
|
||||
case "s":
|
||||
resources.getQuantityString(resId, 1, "ONE");
|
||||
break;
|
||||
case "ds":
|
||||
resources.getQuantityString(resId, 2, 1, "TWO");
|
||||
break;
|
||||
default:
|
||||
if (!TextUtils.isEmpty(formats)) {
|
||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
||||
}
|
||||
}
|
||||
} catch (IllegalFormatException | Resources.NotFoundException e) {
|
||||
Log.i(TAG, locale + " " + field.getName());
|
||||
throw new IllegalArgumentException("Bad '" + formats + "' format in " + locale + " "
|
||||
+ field.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAllStrings() throws IllegalAccessException {
|
||||
Field[] fields = R.string.class.getDeclaredFields();
|
||||
|
||||
HashMap<String, String> haveFormats = new HashMap<>();
|
||||
for (Field field : fields) {
|
||||
String string = resources.getString(field.getInt(int.class));
|
||||
Matcher matcher = androidFormat.matcher(string);
|
||||
int matches = 0;
|
||||
char[] formats = new char[5];
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group(0);
|
||||
char formatType = match.charAt(match.length() - 1);
|
||||
switch (match.length()) {
|
||||
case 2:
|
||||
formats[matches] = formatType;
|
||||
matches++;
|
||||
break;
|
||||
case 4:
|
||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
||||
break;
|
||||
case 5:
|
||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
||||
}
|
||||
}
|
||||
haveFormats.put(field.getName(), new String(formats).trim());
|
||||
}
|
||||
|
||||
for (Locale locale : locales) {
|
||||
config.locale = locale;
|
||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
||||
for (Field field : fields) {
|
||||
int resId = field.getInt(int.class);
|
||||
resources.getString(resId);
|
||||
|
||||
String formats = haveFormats.get(field.getName());
|
||||
try {
|
||||
switch (formats) {
|
||||
case "d":
|
||||
resources.getString(resId, 1);
|
||||
break;
|
||||
case "dd":
|
||||
resources.getString(resId, 1, 2);
|
||||
break;
|
||||
case "ds":
|
||||
resources.getString(resId, 1, "TWO");
|
||||
break;
|
||||
case "dds":
|
||||
resources.getString(resId, 1, 2, "THREE");
|
||||
break;
|
||||
case "sds":
|
||||
resources.getString(resId, "ONE", 2, "THREE");
|
||||
break;
|
||||
case "s":
|
||||
resources.getString(resId, "ONE");
|
||||
break;
|
||||
case "ss":
|
||||
resources.getString(resId, "ONE", "TWO");
|
||||
break;
|
||||
case "sss":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE");
|
||||
break;
|
||||
case "ssss":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE", "FOUR");
|
||||
break;
|
||||
case "ssd":
|
||||
resources.getString(resId, "ONE", "TWO", 3);
|
||||
break;
|
||||
case "sssd":
|
||||
resources.getString(resId, "ONE", "TWO", "THREE", 4);
|
||||
break;
|
||||
default:
|
||||
if (!TextUtils.isEmpty(formats)) {
|
||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, locale + " " + field.getName());
|
||||
throw new IllegalArgumentException("Bad format in '" + locale + "' '" + field.getName() + "': "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeDown;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeLeft;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeRight;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeUp;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.IdlingPolicies;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject;
|
||||
import androidx.test.uiautomator.UiObjectNotFoundException;
|
||||
import androidx.test.uiautomator.UiSelector;
|
||||
|
||||
import org.fdroid.fdroid.views.StatusBanner;
|
||||
import org.fdroid.fdroid.views.main.MainActivity;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MainActivityEspressoTest {
|
||||
public static final String TAG = "MainActivityEspressoTest";
|
||||
|
||||
/**
|
||||
* Emulators older than {@code android-25} seem to fail at running Espresso tests.
|
||||
* <p>
|
||||
* ARM emulators are too slow to run these tests in a useful way. The sad
|
||||
* thing is that it would probably work if Android didn't put up the ANR
|
||||
* "Process system isn't responding" on boot each time. There seems to be no
|
||||
* way to increase the ANR timeout.
|
||||
*/
|
||||
private static boolean canRunEspresso() {
|
||||
if (Build.VERSION.SDK_INT < 25
|
||||
|| Build.SUPPORTED_ABIS[0].startsWith("arm") && isEmulator()) {
|
||||
Log.e(TAG, "SKIPPING TEST: ARM emulators are too slow to run these tests in a useful way");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void classSetUp() {
|
||||
IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.MINUTES);
|
||||
IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.MINUTES);
|
||||
if (!canRunEspresso()) {
|
||||
return;
|
||||
}
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
try {
|
||||
UiDevice.getInstance(instrumentation)
|
||||
.executeShellCommand("pm grant "
|
||||
+ instrumentation.getTargetContext().getPackageName()
|
||||
+ " android.permission.SET_ANIMATION_SCALE");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
|
||||
|
||||
// dismiss the ANR or any other system dialogs that might be there
|
||||
UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
|
||||
try {
|
||||
button.click();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.d(TAG, e.getLocalizedMessage());
|
||||
}
|
||||
new UiWatchers().registerAnrAndCrashWatchers();
|
||||
|
||||
Context context = instrumentation.getTargetContext();
|
||||
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
|
||||
ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
|
||||
activityManager.getMemoryInfo(mi);
|
||||
long percentAvail = mi.availMem / mi.totalMem;
|
||||
Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void classTearDown() {
|
||||
SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
public static boolean isEmulator() {
|
||||
return Build.FINGERPRINT.startsWith("generic")
|
||||
|| Build.FINGERPRINT.startsWith("unknown")
|
||||
|| Build.MODEL.contains("google_sdk")
|
||||
|| Build.MODEL.contains("Emulator")
|
||||
|| Build.MODEL.contains("Android SDK built for x86")
|
||||
|| Build.MANUFACTURER.contains("Genymotion")
|
||||
|| Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|
||||
|| "google_sdk".equals(Build.PRODUCT);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
assumeTrue(canRunEspresso());
|
||||
}
|
||||
|
||||
/**
|
||||
* Placate {@link android.os.StrictMode}
|
||||
*
|
||||
* @see <a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/6f3a38f3afd79ed6dddcef5c83cb442d6749e2ff"> Run finalizers before counting for StrictMode</a>
|
||||
*/
|
||||
@After
|
||||
public void tearDown() {
|
||||
System.gc();
|
||||
System.runFinalization();
|
||||
System.gc();
|
||||
}
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> activityTestRule =
|
||||
new ActivityTestRule<>(MainActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule readExternalStoragePermissionRule = GrantPermissionRule.grant(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
|
||||
@Test
|
||||
public void bottomNavFlavorCheck() {
|
||||
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.menu_settings)).check(matches(isDisplayed()));
|
||||
onView(withText("THIS SHOULD NOT SHOW UP ANYWHERE!!!")).check(doesNotExist());
|
||||
|
||||
assertTrue(BuildConfig.FLAVOR.startsWith("full") || BuildConfig.FLAVOR.startsWith("basic"));
|
||||
|
||||
if (BuildConfig.FLAVOR.startsWith("basic")) {
|
||||
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__categories)).check(doesNotExist());
|
||||
onView(withText(R.string.main_menu__swap_nearby)).check(doesNotExist());
|
||||
}
|
||||
|
||||
if (BuildConfig.FLAVOR.startsWith("full")) {
|
||||
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__categories)).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.main_menu__swap_nearby)).check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showSettings() {
|
||||
ViewInteraction settingsBottonNavButton = onView(
|
||||
allOf(withText(R.string.menu_settings), isDisplayed()));
|
||||
settingsBottonNavButton.perform(click());
|
||||
onView(withText(R.string.preference_manage_installed_apps)).check(matches(isDisplayed()));
|
||||
if (BuildConfig.FLAVOR.startsWith("basic") && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
|
||||
// TODO fix me by sorting out the flavor applicationId for debug builds in app/build.gradle
|
||||
Log.i(TAG, "Skipping the remainder of showSettings test because it just crashes on basic .debug builds");
|
||||
return;
|
||||
}
|
||||
ViewInteraction manageInstalledAppsButton = onView(
|
||||
allOf(withText(R.string.preference_manage_installed_apps), isDisplayed()));
|
||||
manageInstalledAppsButton.perform(click());
|
||||
onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
|
||||
onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.menu_manage)).perform(click());
|
||||
onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
manageInstalledAppsButton.perform(click());
|
||||
onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
|
||||
onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.menu_manage)).perform(click());
|
||||
onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click());
|
||||
|
||||
onView(withText(R.string.about_title)).perform(click());
|
||||
onView(withId(R.id.version)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.ok_button)).perform(click());
|
||||
|
||||
onView(withId(android.R.id.list_container)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showUpdates() {
|
||||
ViewInteraction updatesBottonNavButton = onView(allOf(withText(R.string.main_menu__updates), isDisplayed()));
|
||||
updatesBottonNavButton.perform(click());
|
||||
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showCategories() {
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(allOf(withText(R.string.main_menu__categories), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeRight())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(swipeLeft())
|
||||
.perform(click());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showLatest() {
|
||||
onView(Matchers.instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeUp())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(swipeDown())
|
||||
.perform(click());
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void showSearch() {
|
||||
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
|
||||
onView(withId(R.id.fab_search)).check(doesNotExist());
|
||||
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
|
||||
onView(allOf(withId(R.id.fab_search), isDisplayed())).perform(click());
|
||||
onView(withId(R.id.sort)).check(matches(isDisplayed()));
|
||||
onView(allOf(withId(R.id.search), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(typeText("test"));
|
||||
onView(allOf(withId(R.id.sort), isDisplayed())).perform(click());
|
||||
}
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Replacer for the netstat utility, by reading the /proc filesystem it can find out the
|
||||
* open connections of the system
|
||||
* From http://www.ussg.iu.edu/hypermail/linux/kernel/0409.1/2166.html :
|
||||
* It will first list all listening TCP sockets, and next list all established
|
||||
* TCP connections. A typical entry of /proc/net/tcp would look like this (split
|
||||
* up into 3 parts because of the length of the line):
|
||||
* <p>
|
||||
* 46: 010310AC:9C4C 030310AC:1770 01
|
||||
* | | | | | |--> connection state
|
||||
* | | | | |------> remote TCP port number
|
||||
* | | | |-------------> remote IPv4 address
|
||||
* | | |--------------------> local TCP port number
|
||||
* | |---------------------------> local IPv4 address
|
||||
* |----------------------------------> number of entry
|
||||
* <p>
|
||||
* 00000150:00000000 01:00000019 00000000
|
||||
* | | | | |--> number of unrecovered RTO timeouts
|
||||
* | | | |----------> number of jiffies until timer expires
|
||||
* | | |----------------> timer_active (see below)
|
||||
* | |----------------------> receive-queue
|
||||
* |-------------------------------> transmit-queue
|
||||
* <p>
|
||||
* 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
|
||||
* | | | | | | | | | |--> slow start size threshold,
|
||||
* | | | | | | | | | or -1 if the threshold
|
||||
* | | | | | | | | | is >= 0xFFFF
|
||||
* | | | | | | | | |----> sending congestion window
|
||||
* | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
|
||||
* | | | | | | |---------> Predicted tick of soft clock
|
||||
* | | | | | | (delayed ACK control data)
|
||||
* | | | | | |------------> retransmit timeout
|
||||
* | | | | |------------------> location of socket in memory
|
||||
* | | | |-----------------------> socket reference count
|
||||
* | | |-----------------------------> inode
|
||||
* | |----------------------------------> unanswered 0-window probes
|
||||
* |---------------------------------------------> uid
|
||||
*
|
||||
* @author Ciprian Dobre
|
||||
*/
|
||||
public class Netstat {
|
||||
|
||||
/**
|
||||
* Possible values for states in /proc/net/tcp
|
||||
*/
|
||||
private static final String[] STATES = {
|
||||
"ESTBLSH", "SYNSENT", "SYNRECV", "FWAIT1", "FWAIT2", "TMEWAIT",
|
||||
"CLOSED", "CLSWAIT", "LASTACK", "LISTEN", "CLOSING", "UNKNOWN",
|
||||
};
|
||||
/**
|
||||
* Pattern used when parsing through /proc/net/tcp
|
||||
*/
|
||||
private static final Pattern NET_PATTERN = Pattern.compile(
|
||||
"\\d+:\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+)\\s+" +
|
||||
"[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+\\s+([\\d]+)\\s+[\\d]+\\s+([\\d]+)");
|
||||
|
||||
/**
|
||||
* Utility method that converts an address from a hex representation as founded in /proc to String representation
|
||||
*/
|
||||
private static String getAddress(final String hexa) {
|
||||
try {
|
||||
// first let's convert the address to Integer
|
||||
final long v = Long.parseLong(hexa, 16);
|
||||
// in /proc the order is little endian and java uses big endian order we also need to invert the order
|
||||
final long adr = (v >>> 24) | (v << 24) |
|
||||
((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
|
||||
// and now it's time to output the result
|
||||
return ((adr >> 24) & 0xff) + "." + ((adr >> 16) & 0xff) + "." + ((adr >> 8) & 0xff) + "." + (adr & 0xff);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return "0.0.0.0"; // NOPMD
|
||||
}
|
||||
}
|
||||
|
||||
private static int getInt16(final String hexa) {
|
||||
try {
|
||||
return Integer.parseInt(hexa, 16);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private static String getPName(final int pid) {
|
||||
final Pattern pattern = Pattern.compile("Name:\\s*(\\S+)");
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/" + pid + "/status"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) {
|
||||
// ignored
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method used to question for the connections currently opened
|
||||
*
|
||||
* @return The list of connections (as Connection objects)
|
||||
*/
|
||||
public static List<Connection> getConnections() {
|
||||
|
||||
final ArrayList<Connection> net = new ArrayList<>();
|
||||
|
||||
// read from /proc/net/tcp the list of currently opened socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/tcp"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.TCP_CONNECTION);
|
||||
net.add(c);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
|
||||
// read from /proc/net/udp the list of currently opened socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/udp"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.UDP_CONNECTION);
|
||||
net.add(c);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
|
||||
// read from /proc/net/raw the list of currently opened socket connections
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader("/proc/net/raw"));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) { // NOPMD
|
||||
Matcher matcher = NET_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final Connection c = new Connection();
|
||||
c.setProtocol(Connection.RAW_CONNECTION);
|
||||
net.add(c);
|
||||
//final String localAddressHexa = matcher.group(1);
|
||||
final String localPortHexa = matcher.group(2);
|
||||
final String remoteAddressHexa = matcher.group(3);
|
||||
final String remotePortHexa = matcher.group(4);
|
||||
final String statusHexa = matcher.group(5);
|
||||
//final String uid = matcher.group(6);
|
||||
//final String inode = matcher.group(7);
|
||||
c.setLocalPort(getInt16(localPortHexa));
|
||||
c.setRemoteAddress(getAddress(remoteAddressHexa));
|
||||
c.setRemotePort(getInt16(remotePortHexa));
|
||||
try {
|
||||
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
|
||||
} catch (Exception ex) {
|
||||
c.setStatus(STATES[11]); // unknown
|
||||
}
|
||||
c.setPID(-1); // unknown
|
||||
c.setPName("UNKNOWN");
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
// ignored
|
||||
}
|
||||
return net;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a given connection
|
||||
*
|
||||
* @author Ciprian Dobre
|
||||
*/
|
||||
public static class Connection {
|
||||
|
||||
/**
|
||||
* Types of connection protocol
|
||||
***/
|
||||
static final byte TCP_CONNECTION = 0;
|
||||
static final byte UDP_CONNECTION = 1;
|
||||
static final byte RAW_CONNECTION = 2;
|
||||
/**
|
||||
* <code>serialVersionUID</code>
|
||||
*/
|
||||
private static final long serialVersionUID = 1988671591829311032L;
|
||||
/**
|
||||
* The protocol of the connection (can be tcp, udp or raw)
|
||||
*/
|
||||
protected byte protocol;
|
||||
|
||||
/**
|
||||
* The owner of the connection (username)
|
||||
*/
|
||||
protected String powner;
|
||||
|
||||
/**
|
||||
* The pid of the owner process
|
||||
*/
|
||||
protected int pid;
|
||||
|
||||
/**
|
||||
* The name of the program owning the connection
|
||||
*/
|
||||
protected String pname;
|
||||
|
||||
/**
|
||||
* Local port
|
||||
*/
|
||||
protected int localPort;
|
||||
|
||||
/**
|
||||
* Remote address of the connection
|
||||
*/
|
||||
protected String remoteAddress;
|
||||
|
||||
/**
|
||||
* Remote port
|
||||
*/
|
||||
protected int remotePort;
|
||||
|
||||
/**
|
||||
* Status of the connection
|
||||
*/
|
||||
protected String status;
|
||||
|
||||
public final byte getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
final void setProtocol(final byte protocol) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
final String getProtocolAsString() {
|
||||
switch (protocol) {
|
||||
case TCP_CONNECTION:
|
||||
return "TCP";
|
||||
case UDP_CONNECTION:
|
||||
return "UDP";
|
||||
case RAW_CONNECTION:
|
||||
return "RAW";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
public final String getPOwner() {
|
||||
return powner;
|
||||
}
|
||||
|
||||
public final void setPOwner(final String owner) {
|
||||
this.powner = owner;
|
||||
}
|
||||
|
||||
public final int getPID() {
|
||||
return pid;
|
||||
}
|
||||
|
||||
final void setPID(final int pid) {
|
||||
this.pid = pid;
|
||||
}
|
||||
|
||||
public final String getPName() {
|
||||
return pname;
|
||||
}
|
||||
|
||||
final void setPName(final String pname) {
|
||||
this.pname = pname;
|
||||
}
|
||||
|
||||
public final int getLocalPort() {
|
||||
return localPort;
|
||||
}
|
||||
|
||||
final void setLocalPort(final int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
|
||||
public final String getRemoteAddress() {
|
||||
return remoteAddress;
|
||||
}
|
||||
|
||||
final void setRemoteAddress(final String remoteAddress) {
|
||||
this.remoteAddress = remoteAddress;
|
||||
}
|
||||
|
||||
public final int getRemotePort() {
|
||||
return remotePort;
|
||||
}
|
||||
|
||||
final void setRemotePort(final int remotePort) {
|
||||
this.remotePort = remotePort;
|
||||
}
|
||||
|
||||
public final String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
final void setStatus(final String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[Prot=" + getProtocolAsString() +
|
||||
",POwner=" + powner +
|
||||
",PID=" + pid +
|
||||
",PName=" + pname +
|
||||
",LPort=" + localPort +
|
||||
",RAddress=" + remoteAddress +
|
||||
",RPort=" + remotePort +
|
||||
",Status=" + status +
|
||||
"]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* @see <a href="https://artemzin.com/blog/easiest-way-to-give-set_animation_scale-permission-for-your-ui-tests-on-android/>EASIEST WAY TO GIVE SET_ANIMATION_SCALE PERMISSION FOR YOUR UI TESTS ON ANDROID</a>
|
||||
* @see <a href="https://gist.github.com/xrigau/11284124>Disable animations for Espresso tests</a>
|
||||
*/
|
||||
class SystemAnimations {
|
||||
public static final String TAG = "SystemAnimations";
|
||||
|
||||
private static final float DISABLED = 0.0f;
|
||||
private static final float DEFAULT = 1.0f;
|
||||
|
||||
static void disableAll(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
||||
setSystemAnimationsScale(DISABLED);
|
||||
} else {
|
||||
Log.i(TAG, "Disabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
||||
}
|
||||
}
|
||||
|
||||
static void enableAll(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
||||
setSystemAnimationsScale(DEFAULT);
|
||||
} else {
|
||||
Log.i(TAG, "Enabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSystemAnimationsScale(float animationScale) {
|
||||
try {
|
||||
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
|
||||
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
|
||||
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
|
||||
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
|
||||
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
|
||||
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
|
||||
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
|
||||
|
||||
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
|
||||
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
|
||||
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
|
||||
for (int i = 0; i < currentScales.length; i++) {
|
||||
currentScales[i] = animationScale;
|
||||
}
|
||||
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not change animation scale to " + animationScale + " :'(");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject;
|
||||
import androidx.test.uiautomator.UiObjectNotFoundException;
|
||||
import androidx.test.uiautomator.UiSelector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
class UiWatchers {
|
||||
private static final String LOG_TAG = UiWatchers.class.getSimpleName();
|
||||
private final List<String> mErrors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* We can use the UiDevice registerWatcher to register a small script to be executed when the
|
||||
* framework is waiting for a control to appear. Waiting may be the cause of an unexpected
|
||||
* dialog on the screen and it is the time when the framework runs the registered watchers.
|
||||
* This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
|
||||
* create your own watchers and handle error logging properly for your type of tests.
|
||||
*/
|
||||
void registerAnrAndCrashWatchers() {
|
||||
UiDevice.getInstance().registerWatcher("ANR", () -> {
|
||||
UiObject window = new UiObject(new UiSelector().className(
|
||||
"com.android.server.am.AppNotRespondingDialog"));
|
||||
String errorText = null;
|
||||
if (window.exists()) {
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onAnrDetected(errorText);
|
||||
postHandler("Wait");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
});
|
||||
// class names may have changed
|
||||
UiDevice.getInstance().registerWatcher("ANR2", () -> {
|
||||
UiObject window = new UiObject(new UiSelector().packageName("android")
|
||||
.textContains("isn't responding."));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onAnrDetected(errorText);
|
||||
postHandler("Wait");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
});
|
||||
UiDevice.getInstance().registerWatcher("CRASH", () -> {
|
||||
UiObject window = new UiObject(new UiSelector().className(
|
||||
"com.android.server.am.AppErrorDialog"));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onCrashDetected(errorText);
|
||||
postHandler("OK");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
});
|
||||
UiDevice.getInstance().registerWatcher("CRASH2", () -> {
|
||||
UiObject window = new UiObject(new UiSelector().packageName("android")
|
||||
.textContains("has stopped"));
|
||||
if (window.exists()) {
|
||||
String errorText = null;
|
||||
try {
|
||||
errorText = window.getText();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
Log.e(LOG_TAG, "dialog gone?", e);
|
||||
}
|
||||
onCrashDetected(errorText);
|
||||
postHandler("OK");
|
||||
return true; // triggered
|
||||
}
|
||||
return false; // no trigger
|
||||
});
|
||||
Log.i(LOG_TAG, "Registered GUI Exception watchers");
|
||||
}
|
||||
|
||||
private void onAnrDetected(String errorText) {
|
||||
mErrors.add(errorText);
|
||||
}
|
||||
|
||||
private void onCrashDetected(String errorText) {
|
||||
mErrors.add(errorText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current implementation ignores the exception and continues.
|
||||
*/
|
||||
private void postHandler(String buttonText) {
|
||||
// TODO: Add custom error logging here
|
||||
String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
|
||||
.getInstance().getCurrentPackageName());
|
||||
Log.e(LOG_TAG, formatedOutput);
|
||||
UiObject buttonOK = new UiObject(new UiSelector().text(buttonText).enabled(true));
|
||||
// sometimes it takes a while for the OK button to become enabled
|
||||
buttonOK.waitForExists(5000);
|
||||
try {
|
||||
buttonOK.click();
|
||||
} catch (UiObjectNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package org.fdroid.fdroid.compat;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.fdroid.fdroid.AssetUtils;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/**
|
||||
* This test needs to run on the emulator, even though it technically could
|
||||
* run as a plain JUnit test, because it is testing the specifics of
|
||||
* Android's symlink handling.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FileCompatTest {
|
||||
|
||||
private static final String TAG = "FileCompatTest";
|
||||
|
||||
private SanitizedFile sourceFile;
|
||||
private SanitizedFile destFile;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File dir = getWriteableDir(instrumentation);
|
||||
sourceFile = SanitizedFile.knownSanitized(
|
||||
AssetUtils.copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir));
|
||||
destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct");
|
||||
assertFalse(destFile.exists());
|
||||
assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (!sourceFile.delete()) {
|
||||
Log.w(TAG, "Can't delete " + sourceFile.getAbsolutePath() + ".");
|
||||
}
|
||||
|
||||
if (!destFile.delete()) {
|
||||
Log.w(TAG, "Can't delete " + destFile.getAbsolutePath() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkRuntime() {
|
||||
FileCompat.symlinkRuntime(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkLibcore() {
|
||||
assumeTrue(Build.VERSION.SDK_INT >= 19);
|
||||
FileCompat.symlinkLibcore(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymlinkOs() {
|
||||
assumeTrue(Build.VERSION.SDK_INT >= 21);
|
||||
FileCompat.symlinkOs(sourceFile, destFile);
|
||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer internal over external storage, because external tends to be FAT filesystems,
|
||||
* which don't support symlinks (which we test using this method).
|
||||
*/
|
||||
public static File getWriteableDir(Instrumentation instrumentation) {
|
||||
Context context = instrumentation.getContext();
|
||||
Context targetContext = instrumentation.getTargetContext();
|
||||
|
||||
File[] dirsToTry = new File[]{
|
||||
context.getCacheDir(),
|
||||
context.getFilesDir(),
|
||||
targetContext.getCacheDir(),
|
||||
targetContext.getFilesDir(),
|
||||
context.getExternalCacheDir(),
|
||||
context.getExternalFilesDir(null),
|
||||
targetContext.getExternalCacheDir(),
|
||||
targetContext.getExternalFilesDir(null),
|
||||
Environment.getExternalStorageDirectory(),
|
||||
};
|
||||
|
||||
return getWriteableDir(dirsToTry);
|
||||
}
|
||||
|
||||
private static File getWriteableDir(File[] dirsToTry) {
|
||||
|
||||
for (File dir : dirsToTry) {
|
||||
if (dir != null && dir.canWrite()) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.fdroid.fdroid.AssetUtils;
|
||||
import org.fdroid.fdroid.compat.FileCompatTest;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.index.v2.PermissionV2;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
|
||||
* and checking the listed permissions against the ones specified in apks' AndroidManifest,
|
||||
* which have been specifically generated for this test.
|
||||
* <p>
|
||||
* NOTE: This androidTest cannot run as a Robolectric test because the
|
||||
* required methods from PackageManger are not included in Robolectric's Android API.
|
||||
* java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
|
||||
* at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545)
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ApkVerifierTest {
|
||||
public static final String TAG = "ApkVerifierTest";
|
||||
|
||||
private Instrumentation instrumentation;
|
||||
|
||||
private File sdk14Apk;
|
||||
private File minMaxApk;
|
||||
private File extendedPermissionsApk;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File dir = FileCompatTest.getWriteableDir(instrumentation);
|
||||
assertTrue(dir.isDirectory());
|
||||
assertTrue(dir.canWrite());
|
||||
sdk14Apk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.permissions.sdk14.apk",
|
||||
dir
|
||||
);
|
||||
minMaxApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.permissions.minmax.apk",
|
||||
dir
|
||||
);
|
||||
extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
||||
"org.fdroid.extendedpermissionstest.apk",
|
||||
dir
|
||||
);
|
||||
assertTrue(sdk14Apk.exists());
|
||||
assertTrue(minMaxApk.exists());
|
||||
assertTrue(extendedPermissionsApk.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNulls() {
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(null, null));
|
||||
|
||||
String[] perms = new String[]{"Blah"};
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null));
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMinMax()
|
||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.minmax";
|
||||
apk.targetSdkVersion = 24;
|
||||
ArrayList<String> permissionsList = new ArrayList<>();
|
||||
permissionsList.add("android.permission.READ_CALENDAR");
|
||||
if (Build.VERSION.SDK_INT <= 18) {
|
||||
permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
|
||||
}
|
||||
apk.requestedPermissions = permissionsList.toArray(new String[0]);
|
||||
|
||||
Uri uri = Uri.fromFile(minMaxApk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
|
||||
// still not throwing, because now we only throw when minMaxApk has more permissions than expected
|
||||
permissionsList.add("ADDITIONAL_PERMISSION");
|
||||
apk.requestedPermissions = permissionsList.toArray(new String[0]);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithPrefix() {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG"// implied-permission!
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
apk.requestedPermissions = expectedSet.toArray(new String[0]);
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
|
||||
try {
|
||||
apkVerifier.verifyApk();
|
||||
} catch (ApkVerifier.ApkVerificationException |
|
||||
ApkVerifier.ApkPermissionUnequalException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional permissions are okay. The user is simply
|
||||
* warned about a permission that is not used inside the apk
|
||||
*/
|
||||
@Test
|
||||
public void testAdditionalPermission()
|
||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
apk.requestedPermissions = new String[]{
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
||||
"android.permission.FAKE_NEW_PERMISSION",
|
||||
};
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing permissions are not okay!
|
||||
* The user is then not warned about a permission that the apk uses!
|
||||
*/
|
||||
@Test
|
||||
public void testMissingPermission() {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
apk.requestedPermissions = new String[]{
|
||||
//"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
"android.permission.WRITE_PROFILE",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.NFC",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
||||
};
|
||||
|
||||
Uri uri = Uri.fromFile(sdk14Apk);
|
||||
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
|
||||
try {
|
||||
apkVerifier.verifyApk();
|
||||
fail();
|
||||
} catch (ApkVerifier.ApkVerificationException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
} catch (ApkVerifier.ApkPermissionUnequalException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtendedPerms()
|
||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
||||
HashSet<String> expectedSet = new HashSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.READ_SYNC_STATS",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.WRITE_CALENDAR"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT <= 18) {
|
||||
expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
|
||||
expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= 22) {
|
||||
expectedSet.add("android.permission.GET_ACCOUNTS");
|
||||
expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
|
||||
expectedSet.add("android.permission.MANAGE_ACCOUNTS");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
expectedSet.add("android.permission.CAMERA");
|
||||
if (Build.VERSION.SDK_INT <= 23) {
|
||||
expectedSet.add("android.permission.CALL_PHONE");
|
||||
}
|
||||
}
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "urzip.at.or.at.urzip";
|
||||
ArrayList<PermissionV2> perms = new ArrayList<>();
|
||||
perms.add(new PermissionV2("android.permission.READ_EXTERNAL_STORAGE", 18));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", 18));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.INTERNET", null));
|
||||
perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_SYNC_STATS", null));
|
||||
apk.setRequestedPermissions(perms, 0);
|
||||
ArrayList<PermissionV2> perms23 = new ArrayList<>();
|
||||
perms23.add(new PermissionV2("android.permission.CAMERA", null));
|
||||
perms23.add(new PermissionV2("android.permission.CALL_PHONE", 23));
|
||||
apk.setRequestedPermissions(perms23, 23);
|
||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
String[] expectedPermissions = expectedSet.toArray(new String[0]);
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
|
||||
String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
||||
badPermissions[badPermissions.length - 1] = "notarealpermission";
|
||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
||||
|
||||
Uri uri = Uri.fromFile(extendedPermissionsApk);
|
||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
||||
apkVerifier.verifyApk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImpliedPerms() {
|
||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
||||
"android.permission.WRITE_CALENDAR",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"org.dmfs.permission.READ_TASKS",
|
||||
"org.dmfs.permission.WRITE_TASKS"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT <= 22) { // maxSdkVersion="22"
|
||||
expectedSet.addAll(Arrays.asList(
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS"
|
||||
));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "urzip.at.or.at.urzip";
|
||||
apk.targetSdkVersion = 24;
|
||||
ArrayList<PermissionV2> perms = new ArrayList<>();
|
||||
perms.add(new PermissionV2("android.permission.READ_EXTERNAL_STORAGE", 18));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.INTERNET", null));
|
||||
perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", 22));
|
||||
perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
|
||||
perms.add(new PermissionV2("org.dmfs.permission.READ_TASKS", null));
|
||||
perms.add(new PermissionV2("org.dmfs.permission.WRITE_TASKS", null));
|
||||
apk.setRequestedPermissions(perms, 0);
|
||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
String[] expectedPermissions = expectedSet.toArray(new String[0]);
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
|
||||
expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.GET_ACCOUNTS",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_CALENDAR",
|
||||
"android.permission.WRITE_CONTACTS",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"org.dmfs.permission.READ_TASKS",
|
||||
"org.dmfs.permission.WRITE_TASKS"
|
||||
));
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
|
||||
apk = new Apk();
|
||||
apk.packageName = "urzip.at.or.at.urzip";
|
||||
apk.targetSdkVersion = 23;
|
||||
perms = new ArrayList<>();
|
||||
perms.add(new PermissionV2("android.permission.WRITE_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_NETWORK_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_EXTERNAL_STORAGE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.ACCESS_WIFI_STATE", null));
|
||||
perms.add(new PermissionV2("android.permission.WRITE_CALENDAR", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_CONTACTS", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_SYNC_SETTINGS", null));
|
||||
perms.add(new PermissionV2("android.permission.MANAGE_ACCOUNTS", null));
|
||||
perms.add(new PermissionV2("android.permission.INTERNET", null));
|
||||
perms.add(new PermissionV2("android.permission.AUTHENTICATE_ACCOUNTS", null));
|
||||
perms.add(new PermissionV2("android.permission.GET_ACCOUNTS", null));
|
||||
perms.add(new PermissionV2("android.permission.READ_CALENDAR", null));
|
||||
perms.add(new PermissionV2("org.dmfs.permission.READ_TASKS", null));
|
||||
perms.add(new PermissionV2("org.dmfs.permission.WRITE_TASKS", null));
|
||||
apk.setRequestedPermissions(perms, 0);
|
||||
actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
for (String permission : expectedSet) {
|
||||
if (!actualSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
for (String permission : actualSet) {
|
||||
if (!expectedSet.contains(permission)) {
|
||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
||||
+ Build.VERSION.SDK_INT + ")");
|
||||
}
|
||||
}
|
||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class DnsWithCacheTest {
|
||||
|
||||
private static final String URL_1 = "locaihost";
|
||||
private static final String URL_2 = "fdroid.org";
|
||||
private static final String URL_3 = "fdroid.net";
|
||||
|
||||
private static final InetAddress IP_1;
|
||||
private static final InetAddress IP_2;
|
||||
private static final InetAddress IP_3;
|
||||
|
||||
static {
|
||||
try {
|
||||
IP_1 = InetAddress.getByName("127.0.0.1");
|
||||
IP_2 = InetAddress.getByName("127.0.0.2");
|
||||
IP_3 = InetAddress.getByName("127.0.0.3");
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<InetAddress> LIST_1 = Arrays.asList(IP_1, IP_2, IP_3);
|
||||
private static final List<InetAddress> LIST_2 = Arrays.asList(IP_2);
|
||||
private static final List<InetAddress> LIST_3 = Arrays.asList(IP_3);
|
||||
|
||||
@Test
|
||||
public void basicCacheTest() throws IOException, InterruptedException {
|
||||
// test setup
|
||||
Preferences prefs = Preferences.get();
|
||||
prefs.setDnsCacheEnabledValue(true);
|
||||
DnsCache testObject = DnsCache.get();
|
||||
|
||||
// populate cache
|
||||
testObject.insert(URL_1, LIST_1);
|
||||
testObject.insert(URL_2, LIST_2);
|
||||
testObject.insert(URL_3, LIST_3);
|
||||
|
||||
// check for cached lookup results
|
||||
List<InetAddress> testList = testObject.lookup(URL_1);
|
||||
assertEquals(3, testList.size());
|
||||
assertEquals(IP_1.getHostAddress(), testList.get(0).getHostAddress());
|
||||
assertEquals(IP_2.getHostAddress(), testList.get(1).getHostAddress());
|
||||
assertEquals(IP_3.getHostAddress(), testList.get(2).getHostAddress());
|
||||
|
||||
// toggle preference (false)
|
||||
prefs.setDnsCacheEnabledValue(false);
|
||||
|
||||
// attempt non-cached lookup
|
||||
testList = testObject.lookup(URL_1);
|
||||
assertNull(testList);
|
||||
|
||||
// toggle preference (true)
|
||||
prefs.setDnsCacheEnabledValue(true);
|
||||
|
||||
// confirm lookup results remain in cache
|
||||
testList = testObject.lookup(URL_2);
|
||||
assertEquals(1, testList.size());
|
||||
assertEquals(IP_2.getHostAddress(), testList.get(0).getHostAddress());
|
||||
|
||||
// test removal
|
||||
testList = testObject.remove(URL_2);
|
||||
assertEquals(1, testList.size());
|
||||
assertEquals(IP_2.getHostAddress(), testList.get(0).getHostAddress());
|
||||
testList = testObject.lookup(URL_2);
|
||||
assertNull(testList);
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.test.filters.FlakyTest;
|
||||
|
||||
import org.fdroid.download.DownloadRequest;
|
||||
import org.fdroid.download.HttpDownloader;
|
||||
import org.fdroid.download.HttpManager;
|
||||
import org.fdroid.download.Mirror;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.index.v1.IndexV1UpdaterKt;
|
||||
import org.fdroid.index.v2.IndexV2UpdaterKt;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@FlakyTest
|
||||
public class HttpDownloaderTest {
|
||||
private static final String TAG = "HttpDownloaderTest";
|
||||
|
||||
private final HttpManager httpManager = new HttpManager(
|
||||
Utils.getUserAgent(),
|
||||
FDroidApp.queryString,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
private static final Collection<Pair<String, String>> URLS;
|
||||
|
||||
// https://developer.android.com/reference/javax/net/ssl/SSLContext
|
||||
static {
|
||||
ArrayList<Pair<String, String>> tempUrls = new ArrayList<>(Arrays.asList(
|
||||
new Pair<>("https://f-droid.org/repo", IndexV2UpdaterKt.SIGNED_FILE_NAME),
|
||||
// sites that use SNI for HTTPS
|
||||
new Pair<>("https://mirrors.edge.kernel.org/", "debian/dists/stable/Release"),
|
||||
new Pair<>("https://fdroid.tetaneutral.net/fdroid/repo/", IndexV1UpdaterKt.SIGNED_FILE_NAME),
|
||||
new Pair<>("https://opencolo.mm.fcix.net/fdroid/repo/", "dev.lonami.klooni/en-US/phoneScreenshots/1-game.jpg"),
|
||||
//new Pair<>("https://microg.org/fdroid/repo/index-v1.jar"),
|
||||
//new Pair<>("https://grobox.de/fdroid/repo/index.jar"),
|
||||
new Pair<>("https://guardianproject.info/fdroid/repo", IndexV2UpdaterKt.SIGNED_FILE_NAME),
|
||||
new Pair<>("https://en.wikipedia.org", "/wiki/Index.html"), // no SNI but weird ipv6 lookup issues
|
||||
new Pair<>("https://mirror.cyberbits.eu/fdroid/repo/", IndexV2UpdaterKt.SIGNED_FILE_NAME) // TLSv1.2 only and SNI
|
||||
));
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
// domains that use Let's Encrypt won't work on Android 7.1 and older
|
||||
// https://gitlab.com/fdroid/fdroidclient/-/issues/2102
|
||||
tempUrls = new ArrayList<>(Arrays.asList(
|
||||
new Pair<>("https://cloudflare.f-droid.org/repo", "dev.lonami.klooni/en-US/phoneScreenshots/1-game.jpg")
|
||||
));
|
||||
}
|
||||
URLS = tempUrls;
|
||||
}
|
||||
|
||||
private boolean receivedProgress;
|
||||
|
||||
@Test
|
||||
public void downloadUninterruptedTest() throws IOException, InterruptedException {
|
||||
for (Pair<String, String> pair : URLS) {
|
||||
Log.i(TAG, "URL: " + pair.first + pair.second);
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList(pair.first));
|
||||
DownloadRequest request = new DownloadRequest(pair.second, mirrors, null, null, null);
|
||||
HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.download();
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadUninterruptedTestWithProgress() throws IOException, InterruptedException {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
String path = "index.jar";
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList("https://cloudflare.f-droid.org/repo/"));
|
||||
receivedProgress = false;
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final DownloadRequest request = new DownloadRequest(path, mirrors, null, null, null);
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.setListener((bytesRead, totalBytes) -> {
|
||||
receivedProgress = true;
|
||||
});
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
httpDownloader.download();
|
||||
latch.countDown();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
assertTrue(receivedProgress);
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadHttpBasicAuth() throws IOException, InterruptedException {
|
||||
String path = "myusername/supersecretpassword";
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final DownloadRequest request = new DownloadRequest(path, mirrors, null, "myusername", "supersecretpassword");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.download();
|
||||
assertTrue(destFile.exists());
|
||||
assertTrue(destFile.canRead());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void downloadHttpBasicAuthWrongPassword() throws IOException, InterruptedException {
|
||||
String path = "myusername/supersecretpassword";
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final DownloadRequest request =
|
||||
new DownloadRequest(path, mirrors, null, "myusername", "wrongpassword");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.download();
|
||||
assertFalse(destFile.exists());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void downloadHttpBasicAuthWrongUsername() throws IOException, InterruptedException {
|
||||
String path = "myusername/supersecretpassword";
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList("https://httpbin.org/basic-auth/"));
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final DownloadRequest request =
|
||||
new DownloadRequest(path, mirrors, null, "wrongusername", "supersecretpassword");
|
||||
HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.download();
|
||||
assertFalse(destFile.exists());
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadThenCancel() throws IOException, InterruptedException {
|
||||
final CountDownLatch latch = new CountDownLatch(2);
|
||||
String path = "index.jar";
|
||||
List<Mirror> mirrors = Mirror.fromStrings(Collections.singletonList("https://cloudflare.f-droid.org/repo/"));
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final DownloadRequest request = new DownloadRequest(path, mirrors, null, null, null);
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(httpManager, request, destFile);
|
||||
httpDownloader.setListener((bytesRead, totalBytes) -> {
|
||||
receivedProgress = true;
|
||||
latch.countDown();
|
||||
});
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
httpDownloader.download();
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error downloading: ", e);
|
||||
fail();
|
||||
} catch (InterruptedException e) {
|
||||
// success!
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
||||
httpDownloader.cancelDownload();
|
||||
assertTrue(receivedProgress);
|
||||
destFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package org.fdroid.fdroid.work
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.ListenableWorker.Result
|
||||
import androidx.work.WorkInfo.Companion.STOP_REASON_NOT_STOPPED
|
||||
import androidx.work.WorkInfo.State.ENQUEUED
|
||||
import androidx.work.WorkInfo.State.SUCCEEDED
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.testing.SynchronousExecutor
|
||||
import androidx.work.testing.TestListenableWorkerBuilder
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fdroid.fdroid.AppUpdateManager
|
||||
import org.fdroid.fdroid.FDroidApp
|
||||
import org.fdroid.fdroid.Preferences
|
||||
import org.fdroid.fdroid.Preferences.OVER_NETWORK_ALWAYS
|
||||
import org.fdroid.fdroid.net.ConnectivityMonitorService
|
||||
import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_METERED
|
||||
import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_NO_LIMIT
|
||||
import org.fdroid.fdroid.net.ConnectivityMonitorService.FLAG_NET_UNAVAILABLE
|
||||
import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_APP_UPDATE
|
||||
import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_AUTO_APP_UPDATE
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppUpdateWorkerTest {
|
||||
|
||||
private val context get() = getInstrumentation().targetContext
|
||||
private val workManager get() = WorkManager.getInstance(context)
|
||||
private val preferences: Preferences by lazy { mockk() }
|
||||
private val updateManager: AppUpdateManager by lazy { mockk() }
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// MockKAgentException: Mocking static is supported starting from Android P
|
||||
assumeTrue(SDK_INT >= 28)
|
||||
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setExecutor(SynchronousExecutor())
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
|
||||
mockkStatic(FDroidApp::getAppUpdateManager)
|
||||
every { FDroidApp.getAppUpdateManager(any()) } returns updateManager
|
||||
mockkStatic(Preferences::get)
|
||||
every { Preferences.get() } returns preferences
|
||||
every { preferences.isLocalRepoHttpsEnabled } returns false
|
||||
every { preferences.isOnDemandDownloadAllowed } returns true
|
||||
every { preferences.mirrorErrorData } returns emptyMap<String, Int>()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testHappyPath() {
|
||||
FDroidApp.networkState = FLAG_NET_NO_LIMIT
|
||||
every { updateManager.updateApps() } returns true
|
||||
|
||||
AppUpdateWorker.updateAppsNow(context)
|
||||
|
||||
verify { updateManager.updateApps() }
|
||||
|
||||
val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
|
||||
|
||||
assertEquals(1, workInfo.size)
|
||||
assertEquals(SUCCEEDED, workInfo[0].state)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testException() {
|
||||
every { updateManager.updateApps() } throws IOException("foo bar")
|
||||
|
||||
AppUpdateWorker.updateAppsNow(context)
|
||||
|
||||
val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
|
||||
|
||||
assertEquals(1, workInfo.size)
|
||||
assertEquals(ENQUEUED, workInfo[0].state)
|
||||
assertEquals(STOP_REASON_NOT_STOPPED, workInfo[0].stopReason)
|
||||
|
||||
verify(exactly = 1) { updateManager.updateApps() }
|
||||
|
||||
// build the worker manually, so we can see what result it returns
|
||||
runBlocking {
|
||||
val worker = TestListenableWorkerBuilder<AppUpdateWorker>(context, runAttemptCount = 3)
|
||||
.build()
|
||||
assertEquals(Result.retry(), worker.doWork())
|
||||
}
|
||||
verify(exactly = 2) { updateManager.updateApps() }
|
||||
|
||||
// now build the worker with a higher runAttemptCount
|
||||
runBlocking {
|
||||
val worker = TestListenableWorkerBuilder<AppUpdateWorker>(context, runAttemptCount = 4)
|
||||
.build()
|
||||
assertEquals(Result.failure(), worker.doWork()) // now it fails
|
||||
}
|
||||
verify(exactly = 3) { updateManager.updateApps() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testNotRunningWhenNoNetwork() {
|
||||
mockkStatic(ConnectivityMonitorService::getNetworkState)
|
||||
every { ConnectivityMonitorService.getNetworkState(any()) } returns FLAG_NET_UNAVAILABLE
|
||||
FDroidApp.networkState = FLAG_NET_UNAVAILABLE
|
||||
|
||||
try {
|
||||
AppUpdateWorker.updateAppsNow(context)
|
||||
fail()
|
||||
} catch (e: NullPointerException) {
|
||||
// can't send toast from these tests
|
||||
assertTrue(e.message?.contains("toast") == true)
|
||||
}
|
||||
|
||||
val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
|
||||
assertEquals(0, workInfo.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testNotRunningOnMeteredNetwork() {
|
||||
FDroidApp.networkState = FLAG_NET_METERED
|
||||
every { preferences.isOnDemandDownloadAllowed } returns false
|
||||
|
||||
try {
|
||||
AppUpdateWorker.updateAppsNow(context)
|
||||
fail()
|
||||
} catch (e: NullPointerException) {
|
||||
// can't send toast from these tests
|
||||
assertTrue(e.message?.contains("toast") == true)
|
||||
}
|
||||
|
||||
val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_APP_UPDATE).get()
|
||||
assertEquals(0, workInfo.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testPeriodicWork() {
|
||||
every { preferences.isAutoDownloadEnabled } returns true
|
||||
every { preferences.updateInterval } returns DateUtils.HOUR_IN_MILLIS * 4
|
||||
every { preferences.overWifi } returns OVER_NETWORK_ALWAYS
|
||||
every { preferences.overData } returns OVER_NETWORK_ALWAYS
|
||||
|
||||
AppUpdateWorker.scheduleOrCancel(context)
|
||||
|
||||
val workInfo = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_AUTO_APP_UPDATE).get()
|
||||
assertEquals(1, workInfo.size)
|
||||
assertEquals(ENQUEUED, workInfo[0].state)
|
||||
val id = workInfo[0].id
|
||||
|
||||
every { updateManager.updateApps() } returns true
|
||||
|
||||
val testDriver = WorkManagerTestInitHelper.getTestDriver(context) ?: fail()
|
||||
testDriver.setPeriodDelayMet(id)
|
||||
testDriver.setAllConstraintsMet(id)
|
||||
|
||||
verify { updateManager.updateApps() }
|
||||
|
||||
val workInfo2 = workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME_AUTO_APP_UPDATE)
|
||||
.get()
|
||||
assertEquals(1, workInfo2.size)
|
||||
assertEquals(ENQUEUED, workInfo2[0].state) // stays enqueued for next time
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.compat.FileCompatTest;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* This test cannot run on Robolectric unfortunately since it does not support
|
||||
* getting the timestamps from the files completely.
|
||||
* <p>
|
||||
* This is marked with {@link LargeTest} because it always fails on the emulator
|
||||
* tests on GitLab CI. That excludes it from the test run there.
|
||||
*/
|
||||
@LargeTest
|
||||
public class CleanCacheWorkerTest {
|
||||
public static final String TAG = "CleanCacheWorkerEmulatorTest";
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Rule
|
||||
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
|
||||
|
||||
@Test
|
||||
public void testWorkRequest() throws ExecutionException, InterruptedException {
|
||||
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CleanCacheWorker.class).build();
|
||||
workManagerTestRule.workManager.enqueue(request).getResult();
|
||||
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
|
||||
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearOldFiles() throws IOException, InterruptedException {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
File tempDir = FileCompatTest.getWriteableDir(instrumentation);
|
||||
assertTrue(tempDir.isDirectory());
|
||||
assertTrue(tempDir.canWrite());
|
||||
|
||||
File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
|
||||
FileUtils.deleteQuietly(dir);
|
||||
assertTrue(dir.mkdirs());
|
||||
assertTrue(dir.isDirectory());
|
||||
|
||||
File first = new File(dir, "first");
|
||||
first.deleteOnExit();
|
||||
|
||||
File second = new File(dir, "second");
|
||||
second.deleteOnExit();
|
||||
|
||||
assertFalse(first.exists());
|
||||
assertFalse(second.exists());
|
||||
|
||||
assertTrue(first.createNewFile());
|
||||
assertTrue(first.exists());
|
||||
|
||||
Thread.sleep(7000);
|
||||
assertTrue(second.createNewFile());
|
||||
assertTrue(second.exists());
|
||||
|
||||
CleanCacheWorker.clearOldFiles(dir, 3000); // check all in dir
|
||||
assertFalse(first.exists());
|
||||
assertTrue(second.exists());
|
||||
|
||||
Thread.sleep(7000);
|
||||
CleanCacheWorker.clearOldFiles(second, 3000); // check just second file
|
||||
assertFalse(first.exists());
|
||||
assertFalse(second.exists());
|
||||
|
||||
// make sure it doesn't freak out on a non-existent file
|
||||
File nonexistent = new File(tempDir, "nonexistent");
|
||||
CleanCacheWorker.clearOldFiles(nonexistent, 1);
|
||||
CleanCacheWorker.clearOldFiles(null, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO enable this once getImageCacheDir() can be mocked or provide a writable dir in the test
|
||||
@Test
|
||||
public void testDeleteOldIcons() throws IOException {
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
File imageCacheDir = Utils.getImageCacheDir(context);
|
||||
imageCacheDir.mkdirs();
|
||||
assertTrue(imageCacheDir.isDirectory());
|
||||
File oldIcon = new File(imageCacheDir, "old.png");
|
||||
assertTrue(oldIcon.createNewFile());
|
||||
Assume.assumeTrue("test environment must be able to set LastModified time",
|
||||
oldIcon.setLastModified(System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 370)));
|
||||
File currentIcon = new File(imageCacheDir, "current.png");
|
||||
assertTrue(currentIcon.createNewFile());
|
||||
CleanCacheWorker.deleteOldIcons(context);
|
||||
assertTrue(currentIcon.exists());
|
||||
assertFalse(oldIcon.exists());
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Hans-Christoph Steiner <hans@eds.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* This actually runs {@link FDroidMetricsWorker} on a device/emulator and
|
||||
* submits a report to https://metrics.cleaninsights.org
|
||||
* <p>
|
||||
* This is marked with {@link LargeTest} to exclude it from running on GitLab CI
|
||||
* because it always fails on the emulator tests there. Also, it actually submits
|
||||
* a report.
|
||||
*/
|
||||
@LargeTest
|
||||
public class FDroidMetricsWorkerTest {
|
||||
public static final String TAG = "FDroidMetricsWorkerTest";
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Rule
|
||||
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
|
||||
|
||||
/**
|
||||
* A test for easy manual testing.
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testGenerateReport() throws IOException {
|
||||
String json = FDroidMetricsWorker.generateReport(
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext());
|
||||
System.out.println(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkRequest() throws ExecutionException, InterruptedException {
|
||||
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(FDroidMetricsWorker.class).build();
|
||||
workManagerTestRule.workManager.enqueue(request).getResult();
|
||||
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
|
||||
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.fdroid.fdroid.work;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.work.Configuration;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.testing.SynchronousExecutor;
|
||||
import androidx.work.testing.WorkManagerTestInitHelper;
|
||||
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
|
||||
public class WorkManagerTestRule extends TestWatcher {
|
||||
WorkManager workManager;
|
||||
|
||||
@Override
|
||||
protected void starting(Description description) {
|
||||
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
Context targetContext = instrumentation.getTargetContext();
|
||||
Configuration configuration = new Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setExecutor(new SynchronousExecutor())
|
||||
.build();
|
||||
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration);
|
||||
workManager = WorkManager.getInstance(targetContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.fdroid.install
|
||||
|
||||
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
|
||||
import android.provider.MediaStore.MediaColumns.DISPLAY_NAME
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import mu.KotlinLogging
|
||||
import org.fdroid.install.ApkFileProvider.Companion.getIntent
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.fail
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ApkFileProviderTest {
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val pm = context.packageManager
|
||||
|
||||
private val packageInfoList
|
||||
get() = pm.getInstalledPackages(0).filter {
|
||||
val info = it.applicationInfo ?: return@filter false
|
||||
(info.flags and FLAG_SYSTEM) == 0
|
||||
}.sortedBy {
|
||||
val path = it.applicationInfo?.publicSourceDir ?: return@sortedBy Long.MAX_VALUE
|
||||
File(path).length()
|
||||
}.subList(0, 3) // just test with the three smallest apps
|
||||
|
||||
/**
|
||||
* Test whether reading installed APKs via our custom [android.content.ContentProvider] works.
|
||||
* It also only copies max 3 apps so it doesn't take a long time to run.
|
||||
*/
|
||||
@Test
|
||||
fun testCopyFromGetUri() {
|
||||
for (packageInfo in packageInfoList) {
|
||||
val applicationInfo = packageInfo.applicationInfo ?: fail()
|
||||
val apk = File(applicationInfo.publicSourceDir)
|
||||
val uri = getIntent(packageInfo.packageName).data ?: fail()
|
||||
val test = FileOutputStream("/dev/null")
|
||||
val numBytesCopied = context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
inputStream.copyTo(test)
|
||||
} ?: fail()
|
||||
assertEquals(apk.length(), numBytesCopied)
|
||||
log.info {
|
||||
"${packageInfo.packageName} read $numBytesCopied bytes from ${apk.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether querying the custom [android.content.ContentProvider] for installed APKs
|
||||
* returns the right kind of data.
|
||||
*/
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun testQuery() {
|
||||
for (packageInfo in packageInfoList) {
|
||||
val uri = getIntent(packageInfo.packageName).data ?: fail()
|
||||
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
assertEquals(1, cursor.count)
|
||||
cursor.moveToFirst()
|
||||
val name = cursor.getString(cursor.getColumnIndex(DISPLAY_NAME))
|
||||
assertEquals("${packageInfo.packageName}.apk", name)
|
||||
} ?: fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package org.fdroid.repo
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fdroid.fdroid.data.DBHelper
|
||||
import org.fdroid.fdroid.net.DownloaderFactory
|
||||
import org.fdroid.index.RepoManager
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.slf4j.LoggerFactory.getLogger
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class RepoManagerAddAllIntegrationTest {
|
||||
|
||||
@get:Rule
|
||||
var folder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
private val repos = listOf(
|
||||
"https://raw.githubusercontent.com/2br-2b/Fdroid-repo/master/fdroid/repo",
|
||||
"https://anonymousmessenger.ly/fdroid/repo",
|
||||
"https://fdroid.beocode.eu/fdroid/repo",
|
||||
"https://mobileapp.bitwarden.com/fdroid/repo",
|
||||
"https://briarproject.org/fdroid/repo",
|
||||
"https://fdroid.bromite.org/fdroid/repo",
|
||||
"https://fdroid.gitlab.io/ccc/fdroid/repo",
|
||||
"https://www.collaboraoffice.com/downloads/fdroid/repo",
|
||||
"https://bubu1.eu/cctg/fdroid/repo",
|
||||
"https://static.cryptomator.org/android/fdroid/repo",
|
||||
"https://lucaapp.gitlab.io/fdroid-repository/fdroid/repo",
|
||||
"https://divestos.org/apks/official/fdroid/repo",
|
||||
"https://divestos.org/apks/unofficial/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/efreak/auto-daily-fdroid/main/fdroid/repo",
|
||||
"https://bubu1.eu/fdroidclassic/fdroid/repo",
|
||||
"https://f5a.typed.icu/fdroid/repo",
|
||||
"https://fdroid.fedilab.app/repo",
|
||||
"https://raw.githubusercontent.com/Tobi823/ffupdaterrepo/master/fdroid/repo",
|
||||
"https://rfc2822.gitlab.io/fdroid-firefox/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/Five-Prayers/fdroid-repo-stable/main/fdroid/repo",
|
||||
"https://codeberg.org/florian-obernberger/fdroid-repo/raw/branch/main/repo",
|
||||
"https://fdroid.frostnerd.com/fdroid/repo",
|
||||
"https://pili.qi0.de/fdroid/repo",
|
||||
"https://gitjournal.io/fdroid/repo",
|
||||
"https://guardianproject.info/fdroid/repo",
|
||||
"https://s3.amazonaws.com/guardianproject/fdroid/repo",
|
||||
"https://guardianproject.info/fdroid/repo",
|
||||
"https://f-droid.i2p.io/repo",
|
||||
"https://iitc.app/fdroid/repo",
|
||||
"https://jhass.github.io/insporation/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/iodeOS/fdroid/master/fdroid/repo",
|
||||
"https://apt.izzysoft.de/fdroid/repo",
|
||||
"https://android.izzysoft.de/repo",
|
||||
"https://jak-linux.org/fdroid/repo",
|
||||
"https://julianfairfax.gitlab.io/fdroid-repo/fdroid/repo",
|
||||
"https://kaffeemitkoffein.de/fdroid/repo",
|
||||
"https://store.nethunter.com/repo",
|
||||
"https://cdn.kde.org/android/stable-releases/fdroid/repo",
|
||||
"https://repo.kuschku.de/fdroid/repo",
|
||||
"https://fdroid.libretro.com/repo",
|
||||
"https://fdroid.ltheinrich.de/fdroid/repo",
|
||||
"https://ltt.rs/fdroid/repo",
|
||||
"https://pili.qi0.de/fdroid/repo",
|
||||
"https://fdroid.metatransapps.com/fdroid/repo",
|
||||
"https://microg.org/fdroid/repo",
|
||||
"https://fdroid.mm20.de/repo",
|
||||
"https://repo.mobilsicher.de/fdroid/repo",
|
||||
"https://molly.im/fdroid/repo",
|
||||
"https://molly.im/fdroid/foss/fdroid/repo",
|
||||
"https://f-droid.monerujo.io/fdroid/repo",
|
||||
"https://releases.nailyk.fr/repo",
|
||||
"https://nanolx.org/fdroid/repo",
|
||||
"https://www.nanolx.org/fdroid/repo",
|
||||
"https://repo.netsyms.com/fdroid/repo",
|
||||
"https://archive.newpipe.net/fdroid/repo",
|
||||
"https://repo.nononsenseapps.com/fdroid/repo",
|
||||
"https://fdroid.novy.software/repo",
|
||||
"https://raw.githubusercontent.com/nucleus-ffm/Nucleus-F-Droid-Repo/master/fdroid/repo",
|
||||
"https://obfusk.ch/fdroid/repo",
|
||||
"https://ouchadam.github.io/fdroid-repository/repo",
|
||||
"https://fdroid.partidopirata.com.ar/fdroid/repo",
|
||||
"https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo",
|
||||
"https://fdroid.i2pd.xyz/fdroid/repo",
|
||||
"https://fdroid.rami.io/fdroid/repo",
|
||||
"https://thedoc.eu.org/fdroid/repo",
|
||||
"https://repo.samourai.io/fdroid/repo",
|
||||
"https://fdroid.a3.pm/seabear/repo",
|
||||
"https://raw.githubusercontent.com/jackbonadies/seekerandroid/fdroid/fdroid/repo",
|
||||
"https://fdroid.getsession.org/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/simlar/simlar-fdroid-repo/master/fdroid/repo",
|
||||
"https://s2.spiritcroc.de/fdroid/repo",
|
||||
"https://haagch.frickel.club/files/fdroid/repo",
|
||||
"https://submarine.strangled.net/fdroid/repo",
|
||||
"https://service.tagesschau.de/app/repo",
|
||||
"https://fdroid-repo.calyxinstitute.org/fdroid/repo",
|
||||
"https://releases.threema.ch/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/chrisgch/tca/master/fdroid/repo",
|
||||
"https://fdroid.twinhelix.com/fdroid/repo",
|
||||
"https://secfirst.org/fdroid/repo",
|
||||
"https://fdroid.videlibri.de/repo",
|
||||
"https://guardianproject-wind.s3.amazonaws.com/fdroid/repo",
|
||||
"https://raw.githubusercontent.com/xarantolus/fdroid/main/fdroid/repo",
|
||||
"https://zimbelstern.eu/fdroid/repo",
|
||||
)
|
||||
|
||||
private val log = getLogger(this::class.java.simpleName)
|
||||
private val context = ApplicationProvider.getApplicationContext<Application>()
|
||||
private val db = DBHelper.getDb(context) // real DB
|
||||
private val httpManager = DownloaderFactory.HTTP_MANAGER
|
||||
private val downloaderFactory = DownloaderFactory.INSTANCE
|
||||
|
||||
private val repoManager = RepoManager(context, db, downloaderFactory, httpManager)
|
||||
|
||||
@Before
|
||||
fun optIn() {
|
||||
// Careful! This will add lots of repos to your live DB
|
||||
assumeTrue(false) // don't run integration tests with real repos all the time
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addAllTheThings() = runBlocking {
|
||||
repos.forEach { addRepo(it) }
|
||||
}
|
||||
|
||||
private suspend fun addRepo(url: String) {
|
||||
log.info("Fetching $url")
|
||||
repoManager.fetchRepositoryPreview(url = url, proxy = null)
|
||||
repoManager.addRepoState.test(timeout = 15.seconds) {
|
||||
val fetchState = awaitFinalFetchState()
|
||||
if (fetchState is Fetching && fetchState.fetchResult != null) {
|
||||
repoManager.addFetchedRepository()
|
||||
val item = awaitItem()
|
||||
if (item is Adding) {
|
||||
// await final state
|
||||
assertIs<Added>(awaitItem())
|
||||
} else {
|
||||
// was already final state
|
||||
assertIs<Added>(item)
|
||||
}
|
||||
log.info(" Added")
|
||||
} else if (fetchState is AddRepoError) {
|
||||
log.error(" $fetchState $url")
|
||||
}
|
||||
repoManager.abortAddingRepository()
|
||||
assertIs<None>(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
log.info("End $url")
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<AddRepoState>.awaitFinalFetchState(): AddRepoState {
|
||||
var item = awaitItem()
|
||||
log.info(" $item")
|
||||
while (item is None || (item is Fetching && !item.done)) {
|
||||
item = awaitItem()
|
||||
log.info(" $item")
|
||||
}
|
||||
log.info(" final: $item")
|
||||
return item
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user