Clean up app gradle modules

This commit is contained in:
Torsten Grote
2025-12-29 15:07:21 -03:00
parent 83f8f80332
commit d3164687cd
876 changed files with 966 additions and 980 deletions

View File

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

View File

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

View File

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

View File

@@ -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 +
"]";
}
}
}

View File

@@ -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 + " :'(");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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