Compare commits

..

No commits in common. "master" and "db-version/81" have entirely different histories.

1627 changed files with 17894 additions and 40198 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
*.gpg binary

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ build.xml
# Gradle files
.gradle/
build/
gradle.properties
# Local configuration file (sdk path, etc)
local.properties

View File

@ -1,36 +1,26 @@
stages:
- test
- deploy
image: registry.gitlab.com/fdroid/ci-images-client:latest
.base:
image: registry.gitlab.com/fdroid/ci-images-client:latest
before_script:
- export GRADLE_USER_HOME=$PWD/.gradle
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- alias sdkmanager="sdkmanager --no_https"
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
# limit RAM usage for all gradle runs
- export maxmem=$(expr $(sed -n 's,^MemAvailable:[^0-9]*\([0-9][0-9]*\)[^0-9]*$,\1,p' /proc/meminfo) / 1024 / 2 / 1024 \* 1024)
- printf "\norg.gradle.jvmargs=-Xmx${maxmem}m -XX:MaxPermSize=${maxmem}m\norg.gradle.daemon=false\norg.gradle.parallel=false\n" >> gradle.properties
after_script:
# this file changes every time but should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
cache:
cache:
paths:
- .gradle/wrapper
- .gradle/caches
stages:
- test
- deploy
before_script:
- export GRADLE_USER_HOME=$PWD/.gradle
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- alias sdkmanager="sdkmanager --no_https"
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
.test-template: &test-template
extends: .base
stage: test
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- kernel.log
- logcat.txt
- app/core*
- app/*.log
- app/build/reports
- app/build/outputs/*ml
- app/build/outputs/apk
@ -45,40 +35,29 @@ test_lint_pmd_checkstyle:
<<: *test-template
script:
- export EXITVALUE=0
- function set_error() { export EXITVALUE=1; printf "\x1b[31mERROR `history|tail -2|head -1|cut -b 6-500`\x1b[0m\n"; }
- ./gradlew assemble
# always report on lint errors to the build log
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle
- ./gradlew testFullDebugUnitTest || set_error
- ./gradlew lint || set_error
- ./gradlew pmd || set_error
- ./gradlew checkstyle || set_error
- ./tools/check-format-strings.py || set_error
- ./tools/check-fastlane-whitespace.py || set_error
- ./tools/remove-unused-and-blank-translations.py || set_error
- ./gradlew testFullDebugUnitTest
- ./gradlew lint
- ./gradlew pmd || export EXITVALUE=1
- ./gradlew checkstyle || export EXITVALUE=1
- ./tools/check-format-strings.py || export EXITVALUE=1
- ./tools/check-fastlane-whitespace.py || export EXITVALUE=1
- ./tools/remove-unused-and-blank-translations.py || export EXITVALUE=1
- echo "These are unused or blank translations that should be removed:"
- git --no-pager diff --ignore-all-space --name-only --exit-code app/src/*/res/values*/strings.xml || set_error
- git --no-pager diff --ignore-all-space --name-only --exit-code app/src/*/res/values*/strings.xml || export EXITVALUE=1
- exit $EXITVALUE
errorprone:
extends: .base
stage: test
script:
- apt-get update
- apt-get install -t stretch-backports openjdk-11-jdk-headless
- update-java-alternatives --set java-1.11.0-openjdk-amd64
- export JAVA_HOME=/usr/lib/jvm/java-1.11.0-openjdk-amd64
- cat config/errorprone.gradle >> app/build.gradle
- ./gradlew -Dorg.gradle.dependency.verification=lenient assembleDebug
- ./gradlew assembleDebug
# Run the tests in the emulator. Each step is broken out to run on
# its own since the CI runner can have limited RAM, and the emulator
# can take a while to start.
#
# once these prove stable, the task should be switched to
# connectedCheck to test all the build flavors
.connected-template: &connected-template
extends: .base
script:
- ./gradlew assembleFullDebug
- export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'`
@ -86,23 +65,39 @@ errorprone:
- export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'`
- export AVD_PACKAGE="system-images;android-${AVD_SDK};${AVD_TAG};${AVD_ARCH}"
- echo $AVD_PACKAGE
- emulator -accel-check || true
- alias sdkmanager
- ls -l ~/.android
- echo y | sdkmanager "emulator" "platforms;android-$AVD_SDK" > /dev/null
- if ! avdmanager list avd | grep "Name. avd$AVD_SDK$"; then
set -x;
rm -rf ~/.android/avd $ANDROID_HOME/system-images;
echo y | sdkmanager "$AVD_PACKAGE" > /dev/null;
echo no | avdmanager create avd --name avd$AVD_SDK --tag "$AVD_TAG" --package "$AVD_PACKAGE" --sdcard 64M --device "Nexus 5";
export RAMSIZE="$((`sed -n 's,^MemAvailable:[^0-9]*\([0-9][0-9]*\)[^0-9]*$,\1,p' /proc/meminfo` * 3 / 4 / 1024))";
sed -i '/^hw\.ramSize\s*=.*/d' ~/.android/avd/*.avd/config.ini;
echo "hw.ramSize=$RAMSIZE" >> ~/.android/avd/*.avd/config.ini;
avdmanager list avd;
set +x;
fi
- adb start-server
- start-emulator
- ls -l ~/.android
- emulator -avd avd$AVD_SDK
-no-audio
-no-jni
-no-snapstorage
-no-window
-skip-adb-auth
-verbose
-wipe-data
&
- wait-for-emulator
- adb devices
- adb shell input keyevent 82 &
- ./gradlew installFullDebug
- adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity
- if [ $AVD_SDK -lt 25 ] || ! emulator -accel-check; then
export FLAG=-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest;
fi
- ./gradlew connectedFullDebugAndroidTest $FLAG
- test $AVD_SDK -ge 25 || export FLAG=-Pandroid.testInstrumentationRunnerArguments.notAnnotation=android.test.suitebuilder.annotation.LargeTest
- ./gradlew connectedFullDebugAndroidTest $FLAG || (adb -e logcat -d > logcat.txt; exit 1)
no-accel 22 default x86:
connected 22 default armeabi-v7a:
retry: 1
<<: *test-template
<<: *connected-template
@ -110,17 +105,35 @@ no-accel 22 default x86:
tags:
- fdroid
- kvm
allow_failure: true
only:
variables:
- $RUN_KVM_JOBS
- branches@eighthave/fdroidclient
<<: *test-template
<<: *connected-template
kvm 29 microg x86_64:
connected 17 default x86:
<<: *kvm-template
connected 18 default x86:
<<: *kvm-template
connected 23 default x86:
<<: *kvm-template
connected 25 default x86:
<<: *kvm-template
connected 26 google_apis x86:
<<: *kvm-template
connected 27 google_apis_playstore x86:
<<: *kvm-template
connected 28 default x86_64:
<<: *kvm-template
deploy_nightly:
extends: .base
stage: deploy
only:
- master
@ -140,3 +153,8 @@ deploy_nightly:
# build the APKs!
- ./gradlew assembleDebug
- fdroid nightly -v
after_script:
# this file changes every time but should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/

View File

@ -1,3 +0,0 @@
[weblate]
url = https://hosted.weblate.org/api/
translation = f-droid/f-droid

View File

@ -1,229 +1,3 @@
### 1.13-alpha1 (2021-06-02)
* Stop repeated updates of Trichrome Library
* More changes to follow Material Design (@proletarius101)
* Improve OpenCollective badge (@ConnyDuck)
### 1.13-alpha0 (2021-04-22)
* Theme support tied to built-in Android themes (@proletarius101)
* New top banner notifications: "No Internet" and "No Data or WiFi enabled"
* Improved handling of USB-OTG and SD Card repos and mirrors
### 1.12.1 (2021-04-12)
* Fix trove4j verification error
### 1.12 (2021-04-06)
* Sync translations
### 1.12-alpha3 (2021-03-10)
* Opt-in F-Droid Metrics
### 1.12-alpha2 (2021-03-03)
* Overhaul clean up of cached files
* Support updating "shared library packages" like Trichrome (@uldiniad)
### 1.12-alpha1 (2021-02-25)
* Add extra sanitation to search terms to prevent vulnerabilities.
* Fix Nearby Swap's close button (@proletarius101)
* Bump to compileSdkVersion 29 to support Java8
* Set up WorkManager on demand to avoid slowing down starts
* Prefer system keys when APKs are signed by them (@glennmen)
### 1.12-alpha0 (2021-02-08)
* App description localization now fully respects lists of languages in Android
Language Settings
* Latest Tab lists results based on the Language Settings
* Latest Tab now shows results ordered newest first (@TheLastProject @IzzySoft)
* Theme support modernized and tied to the built-in Android themes (@proletarius101)
* Search results greatly improved (@Tvax @gcbrown76)
* Let Android efficiently schedule background cache cleanup operations (@Isira-Seneviratne)
* Overhaul repo URL parsing for reliable repo adding (@projectgus)
### 1.11 (2020-12-29)
* Improved linkifying of URLs in app descriptions
* Improved handling of SDCards and USG-OTG in Nearby
* Modernized code and switched PNGs to vectors (thanks @isira-seneviratne!)
* Recognize longer repo URLs to support GitCDN/RawGit/etc mirrors
### 1.10 (2020-10-20)
* Improved language selection with multiple locales
(thanks @spacecowboy and @bubu!)
### 1.10-alpha1 (2020-09-29)
* use notification channels for fine-grained control (@Isira-Seneviratne)
### 1.10-alpha0 (2020-07-20)
* Latest Tab will show better results on non-English devices
* updates to core libraries (Jackson, androidx, gradle, etc)
* use Gradle's new dependency verification
* polish whitelabeling support
### 1.9 (2020-06-25)
* Removed "Android App Link" support since it cannot work with
F-Droid, and it was triggering DNS leaks.
* Archive Repos are now lower priority than the Repo (higher on the
Manage Repos screen), fixing issues where it looked for icons,
screenshots and other information in the Archive rather than the
Repo itself.
* Fixed hopefully all occurrences where F-Droid client couldn't show an icon.
The remaining cases of missing icons are now caused either by
icons not included in upstream repo or by temporary network failures.
(After updating this requires one additional repo update to take effect.)
* Fixed a problem where repository updates would never trigger
when either "Over Data" or "Over Wifi" were disabled.
* Support OpenCollective donation option and highlight
free software donation platforms
* Fix for when the app update button wasn't showing up or working
in some cases (thanks @di72nn)
* Stop cropping feature header image (thanks @ByteHamster!)
* Make navigation bar match dark mode (thanks @MatthieuB!)
* Cleaned out obsolete code (thanks @Isira-Seneviratne!)
### 1.8-alpha2 (2020-02-04)
* stop showing Unknown Sources with Privileged Extension on Android 10 #1833
* add standard ripple effect to links on app details activity
* fix displaying default icon for apps without icons
### 1.8-alpha1 (2020-01-10)
* handle Android 10 permission config to stop Unknown Sources prompts
* keyboard opens when search is cleared
* translation sync with Android strings
* force common repo domains to HTTPS (GitLab, GitHub, Amazon)
### 1.8-alpha0 (2019-11-20)
* fix seekbar preference on recent Android versions (thanks @dkanada)
* handle API 29 split-permissions: fine location now implies coarse location
* define backup rules to avoid saving the swap repo
### 1.7.1 (2019-07-31)
* fix crashes from ACRA report emails
### 1.7 (2019-07-06)
* fix crash in Panic Settings
* catch random crashes related to WifiApControl
### 1.7-alpha2 (2019-06-18)
* USB OTG flash drives can be used as nearby repos and mirrors
### 1.7-alpha1 (2019-06-14)
* overhauled nearby swap using the device's hotspot AP
* add new panic responses: app uninstalls and reset repos to default
* fix proxy support on first start
### 1.7-alpha0 (2019-05-20)
* major refactor of "Nearby" UI code, to prepare for rewriting guts
* show "undo" after swiping away items from the Updates tab (thanks @Hocuri!)
* fix ETag handling when connecting to nginx mirrors #1737
* fix issues with "Latest" display caused by mishandling time zones #1757
* ignore all unimportant crashes in background services
* do not use Privileged Extension if it was disabled in Settings
### 1.6.2 (2019-05-20)
* fixed issue where cached indexes were wrongly redownloaded (#1737),
thanks to @amiraliakbari for tracking it down!
* fixed wrong string for the translated title of the Updates Tab (#1785)
* fixed crashes on very low memory when starting
### 1.6.1 (2019-05-10)
* Updated translations
* fixed button size issues #1678
* stopped random background crashes
### 1.6 (2019-04-10)
* update F-Droid after all other updates (#1556)
* Improve adding repos from the clipboard (e.g. Firefox Klar)
* swap usability improvements
* many crash fixes in swap and background services
### 1.6-alpha2 (2019-03-28)
* Latest Tab now highlights apps that provide descriptions,
translations, screenshots
* Auto-download from mirrors, to speed up downloads and reduce load on
f-droid.org
* More efficient download caching (per-repo; across different
webservers #1708)
* Fix problems canceling downloads (#1727, #1736, #1742)
* Fix downloading OBB files from repos (#1403)
### 1.6-alpha1 (2019-02-20)
* add switches in RepoDetails to disable any or all mirrors (#1696)

View File

@ -26,16 +26,14 @@ track of modifications and fuzzy translations. Applying translations manually
skips all of the fixes and checks, and overrides the fuzzy state of strings.
Note that you cannot change the English strings on Weblate. If you have any
suggestions on how to improve them, open an issue or merge request like you
would if you were making code changes. This way the changes can be reviewed
before the source strings on Weblate are changed.
suggestions on how to improve them, open a merge request like you would if you
were making code changes. This way the changes can be reviewed before the
source strings on Weblate are changed.
## Code Style
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This
should be more or less the same as [Android Java
style](https://source.android.com/source/code-style.html). Some key points:
We follow the [Android Java style](https://source.android.com/source/code-style.html).
Some key points:
* Four space indentation
* UTF-8 source files
@ -47,35 +45,76 @@ style](https://source.android.com/source/code-style.html). Some key points:
* Braces are always used after if, for and while
The current code base doesn't follow it entirely, but new code should follow
it. We enforce some of these, but not all, via `./gradlew checkstyle`.
it. We enforce some of these, but not all, via checkstyle.
## Debugging
To get all the logcat messages by F-Droid, you can run:
adb logcat | grep `adb shell ps | grep org.fdroid.fdroid | cut -c10-15`
## Building tips
* Use gradle with `--daemon` if you are going to build F-Droid multiple times.
* If you get a message like `Could not find com.android.support:support-...`,
make sure that you have the latest Android support maven repository.
* When building as part of AOSP with `Android.mk`, make sure you have a
recent version of Gradle installed as `gradlew` will not be used.
## Running the test suite
Before pushing commits to a merge request, make sure this passes:
./gradlew checkstyle pmd lint
In order to run the F-Droid test suite, you will need to have either a real device
connected via `adb`, or an emulator running. Then, execute the following from the
command line:
./gradlew check
Many important tests require a device or emulator, but do not work in GitLab CI.
That mean they need to be run locally, and that is usually easiest in Android
Studio rather than the command line.
Note that the CI already runs the tests on an emulator, so you don't
necessarily have to do this yourself if you open a merge request as the tests
will get run.
For a quick way to run a specific JUnit/Robolectric test:
### Running tests in Android Studio
./gradlew testFullDebugUnitTest --tests *LocaleSelectionTest*
Later versions of Android Studio require tests to be run with a "Working directory"
of `$MODULE_DIR$`.
[To make this the default behaviour](https://code.google.com/p/android/issues/detail?id=158015#c11),
close any projects to get the Welcome dialog. Then choose _Configure > Project Defaults >
Run Configurations > Defaults > Android JUnit_, and change "Working directory"
to `$MODULE_DIR$`. If you already have a project setup in Android Studio, you
may also need to change the default in _Run > Edit Configurations... > Defaults >
Android JUnit_.
For a quick way to run a specific emulator test:
## Versioning
./gradlew connectedFullDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest
Each stable version follows the `X.Y` pattern. Hotfix releases - i.e. when a
stable has an important bug that needs immediate fixing - will follow the
`X.Y.Z` pattern.
Before each stable release, a number of alpha releases will be released. They
will follow the pattern `X.Y-alphaN`, where `N` is the current alpha number.
These will usually include changes and new features that have not been tested
enough for a stable release, so use at your own risk. Testers and reporters
are very welcome.
## Making releases
The version codes use a number of digits per each of these keys: `XXXYYYZNN`.
So for example, 1.3.1 would be `1003150` and 0.95-alpha13 would be `95013`
(leading zeros are omitted).
See https://gitlab.com/fdroid/wiki/-/wikis/Internal/Release-Process#fdroidclient
Note that we use a trailing `50` for actual stable releases, so alphas are
limited to `-alpha49`.
This is an example of a release process for **0.95**:
* We are currently at stable **0.94**
* **0.95-alpha1** is released
* **0.95-alpha2** is released
* **0.95-alpha3** is released
* `stable-v0.95` is branched and frozen
* **0.95** is released
* A bug is reported on the stable release and fixed
* **0.95.1** is released with only that fix
As soon as a stable is tagged, master will move on to `-alpha0` on the next
version. This is a temporary measure - until `-alpha1` is released - so that
moving from stable to master doesn't require a downgrade. `-alpha0` versions
will not be tagged nor released.

View File

@ -1,11 +0,0 @@
---
liberapay: F-Droid-Data
open_collective: F-Droid
github:
- f-droid
- eighthave
custom:
- https://f-droid.org/donate/
- https://www.hellotux.com/f-droid
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E2FCXCT6837GL
- https://blockchain.info/address/15u8aAPK4jJ5N8wpWJ5gutAyyeHtKX5i18

View File

@ -1,6 +1,6 @@
# F-Droid Client
[![build status](https://gitlab.com/fdroid/fdroidclient/badges/master/pipeline.svg)](https://gitlab.com/fdroid/fdroidclient/-/jobs)
[![build status](https://gitlab.com/fdroid/fdroidclient/badges/master/build.svg)](https://gitlab.com/fdroid/fdroidclient/builds)
[![Translation status](https://hosted.weblate.org/widgets/f-droid/-/svg-badge.svg)](https://hosted.weblate.org/engage/f-droid/)
Client for [F-Droid](https://f-droid.org), the Free Software repository system

View File

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'checkstyle'
apply plugin: 'pmd'
/* gets the version name from the latest Git tag */
/* gets the version name from the latest Git tag, stripping the leading v off */
def getVersionName = { ->
def stdout = new ByteArrayOutputStream()
exec {
@ -12,8 +12,8 @@ def getVersionName = { ->
return stdout.toString().trim()
}
def isCi = "true" == System.getenv("CI")
def preDexEnabled = "true" == System.getProperty("pre-dex", "true")
def isCi = "true".equals(System.getenv("CI"))
def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
def fullApplicationId = "org.fdroid.fdroid"
def basicApplicationId = "org.fdroid.basic"
@ -21,16 +21,14 @@ def basicApplicationId = "org.fdroid.basic"
def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
android {
compileSdkVersion 30
compileSdkVersion 27
buildToolsVersion '27.0.3'
defaultConfig {
versionCode 1013001
versionCode 1006001
versionName getVersionName()
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
minSdkVersion 24
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
/*
The Android Testing Support Library collects analytics to continuously improve the testing
experience. More specifically, it uploads a hash of the package name of the application
@ -38,7 +36,6 @@ android {
passing the following argument to the test runner: disableAnalytics "true".
*/
testInstrumentationRunnerArguments disableAnalytics: 'true'
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -46,9 +43,9 @@ android {
// release builds before.
all {
minifyEnabled true
useProguard true
shrinkResources true
buildConfigField "String", "PRIVILEGED_EXTENSION_PACKAGE_NAME", privilegedExtensionApplicationId
buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"' // String needs both quotes
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
}
@ -76,8 +73,6 @@ android {
compileOptions {
compileOptions.encoding = "UTF-8"
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
aaptOptions {
@ -102,10 +97,6 @@ android {
events "skipped", "failed", "standardOut", "standardError"
showStandardStreams = true
}
systemProperty 'robolectric.dependency.repo.url', 'https://repo1.maven.org/maven2'
// hack to avoid memory leak crashes
forkEvery = 1
}
}
}
@ -142,56 +133,51 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.work:work-runtime:2.4.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:gridlayout-v7:27.1.1'
implementation 'com.android.support:support-annotations:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:support-vector-drawable:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:palette-v7:27.1.1'
implementation 'com.android.support:preference-v14:27.1.1'
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'com.google.zxing:core:3.3.3'
implementation 'info.guardianproject.netcipher:netcipher:2.2.0-alpha'
implementation 'info.guardianproject.panic:panic:1.0'
implementation 'info.guardianproject.netcipher:netcipher:2.0.0-beta1'
implementation 'info.guardianproject.panic:panic:0.5'
implementation 'commons-io:commons-io:2.6'
implementation 'commons-net:commons-net:3.6'
implementation 'ch.acra:acra:4.9.1'
implementation 'io.reactivex:rxjava:1.1.0'
implementation 'io.reactivex:rxandroid:0.23.0'
implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1'
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxjava:3.0.9'
implementation 'com.fasterxml.jackson.core:jackson-core:2.8.11'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.8.11'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.11'
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
fullImplementation 'org.jmdns:jmdns:3.5.5'
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'org.mockito:mockito-core:3.3.3'
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.65'
testImplementation 'org.robolectric:robolectric:3.8'
testImplementation "com.android.support.test:monitor:1.0.2"
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.7.22'
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:monitor:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'androidx.work:work-testing:2.4.0'
androidTestImplementation 'com.android.support:support-annotations:27.1.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
}
checkstyle {
@ -207,7 +193,7 @@ task checkstyle(type: Checkstyle) {
}
pmd {
toolVersion = '6.20.0'
toolVersion = '5.5.1'
consoleOutput = true
}
@ -217,6 +203,7 @@ task pmdMain(type: Pmd) {
ruleSets = [] // otherwise defaults clash with the list in rules.xml
source 'src/main/java'
include '**/*.java'
exclude '**/kellinwood/**/*.java'
}
task pmdTest(type: Pmd) {

View File

@ -40,8 +40,6 @@
<issue id="PluralsCandidate" severity="error"/>
<issue id="HardcodedText" severity="error"/>
<issue id="RtlCompat" severity="error"/>
<issue id="RtlEnabled" severity="error"/>
<!-- both the correct and deprecated locales need to be present for
them to be recognized on all devices -->

View File

@ -4,11 +4,13 @@
-keep class org.fdroid.fdroid.** {*;}
-dontskipnonpubliclibraryclassmembers
-dontwarn android.test.**
-dontwarn com.android.support.test.**
-dontwarn javax.naming.**
-dontwarn org.slf4j.**
-dontnote org.apache.http.**
-dontnote android.net.http.**
-dontnote android.support.**
-dontnote **ILicensingService
# Needed for espresso https://stackoverflow.com/a/21706087
@ -31,6 +33,24 @@
public *;
}
# Samsung Android 4.2 bug
# https://code.google.com/p/android/issues/detail?id=78377
-keepnames class !android.support.v7.internal.view.menu.**, ** {*;}
-keep public class android.support.v7.widget.** {*;}
-keep public class android.support.v7.internal.widget.** {*;}
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# The rxjava library depends on sun.misc.Unsafe, which is unavailable on Android
# The rxjava team is aware of this, and mention in the docs that they only use
# the unsafe functionality if the platform supports it.
# - https://github.com/ReactiveX/RxJava/issues/1415#issuecomment-48390883
# - https://github.com/ReactiveX/RxJava/blob/1.x/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java#L23
-dontwarn rx.internal.util.**
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.ext.**
@ -40,8 +60,3 @@ public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; }
-keep public class your.class.** {
*;
}
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
public <init>(...);
}

View File

@ -1,24 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.fdroid.fdroid.tests"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
<uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
<application>
<uses-library
android:name="android.test.runner"
android:required="false" />
<uses-library android:name="android.test.runner"/>
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>

View File

@ -1,8 +1,8 @@
package org.fdroid.fdroid;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
@ -16,9 +16,6 @@ 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;
@ -31,7 +28,6 @@ public class AssetUtils {
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);

View File

@ -0,0 +1,65 @@
package org.fdroid.fdroid;
import android.app.Instrumentation;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.compat.FileCompatTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class CleanCacheServiceTest {
public static final String TAG = "CleanCacheServiceTest";
@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());
CleanCacheService.clearOldFiles(dir, 3000); // check all in dir
assertFalse(first.exists());
assertTrue(second.exists());
Thread.sleep(7000);
CleanCacheService.clearOldFiles(second, 3000); // check just second file
assertFalse(first.exists());
assertFalse(second.exists());
// make sure it doesn't freak out on a non-existant file
File nonexistant = new File(tempDir, "nonexistant");
CleanCacheService.clearOldFiles(nonexistant, 1);
CleanCacheService.clearOldFiles(null, 1);
}
}

View File

@ -5,8 +5,8 @@ import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
@ -179,9 +179,6 @@ public class LocalizationTest {
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;

View File

@ -5,23 +5,20 @@ import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.os.Build;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.espresso.IdlingPolicies;
import androidx.test.espresso.ViewInteraction;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.IdlingPolicies;
import android.support.test.espresso.ViewInteraction;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.rule.GrantPermissionRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
import android.util.Log;
import android.view.View;
import org.fdroid.fdroid.views.StatusBanner;
import org.fdroid.fdroid.views.BannerUpdatingRepos;
import org.fdroid.fdroid.views.main.MainActivity;
import org.hamcrest.Matchers;
import org.junit.After;
@ -35,25 +32,23 @@ import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
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 android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeDown;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityEspressoTest {
public static final String TAG = "MainActivityEspressoTest";
@ -91,7 +86,7 @@ public class MainActivityEspressoTest {
} catch (IOException e) {
e.printStackTrace();
}
SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
SystemAnimations.disableAll(InstrumentationRegistry.getTargetContext());
// dismiss the ANR or any other system dialogs that might be there
UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
@ -104,7 +99,7 @@ public class MainActivityEspressoTest {
Context context = instrumentation.getTargetContext();
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryInfo(mi);
long percentAvail = mi.availMem / mi.totalMem;
Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
@ -112,7 +107,7 @@ public class MainActivityEspressoTest {
@AfterClass
public static void classTearDown() {
SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
SystemAnimations.enableAll(InstrumentationRegistry.getTargetContext());
}
public static boolean isEmulator() {
@ -177,7 +172,6 @@ public class MainActivityEspressoTest {
}
@LargeTest
@Test
public void showSettings() {
ViewInteraction settingsBottonNavButton = onView(
allOf(withText(R.string.menu_settings), isDisplayed()));
@ -192,27 +186,9 @@ public class MainActivityEspressoTest {
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(R.string.abc_action_bar_up_description)).perform(click());
onView(withText(R.string.menu_manage)).perform(click());
onView(withContentDescription(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(R.string.abc_action_bar_up_description)).perform(click());
onView(withText(R.string.menu_manage)).perform(click());
onView(withContentDescription(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());
@ -220,7 +196,6 @@ public class MainActivityEspressoTest {
}
@LargeTest
@Test
public void startSwap() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
@ -229,14 +204,12 @@ public class MainActivityEspressoTest {
allOf(withText(R.string.main_menu__swap_nearby), isDisplayed()));
nearbyBottonNavButton.perform(click());
ViewInteraction findPeopleButton = onView(
allOf(withId(R.id.find_people_button), withText(R.string.nearby_splash__find_people_button),
isDisplayed()));
allOf(withId(R.id.find_people_button), withText(R.string.nearby_splash__find_people_button), isDisplayed()));
findPeopleButton.perform(click());
onView(withText(R.string.swap_send_fdroid)).check(matches(isDisplayed()));
}
@LargeTest
@Test
public void showCategories() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
@ -262,12 +235,11 @@ public class MainActivityEspressoTest {
}
@LargeTest
@Test
public void showLatest() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
}
onView(Matchers.<View>instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
onView(Matchers.<View>instanceOf(BannerUpdatingRepos.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()))
@ -285,7 +257,6 @@ public class MainActivityEspressoTest {
}
@LargeTest
@Test
public void showSearch() {
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
onView(withId(R.id.fab_search)).check(doesNotExist());

View File

@ -1,372 +0,0 @@
package org.fdroid.fdroid;
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 treshold
* | | | | | | | | | 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 openned
*
* @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 openned 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 openned 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 openned 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;
}
/**
* Informations about a given connection
*
* @author Ciprian Dobre
*/
public static class Connection {
/**
* Types of connection protocol
***/
public static final byte TCP_CONNECTION = 0;
public static final byte UDP_CONNECTION = 1;
public 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;
}
public final void setProtocol(final byte protocol) {
this.protocol = protocol;
}
public 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;
}
public final void setPID(final int pid) {
this.pid = pid;
}
public final String getPName() {
return pname;
}
public final void setPName(final String pname) {
this.pname = pname;
}
public final int getLocalPort() {
return localPort;
}
public final void setLocalPort(final int localPort) {
this.localPort = localPort;
}
public final String getRemoteAddress() {
return remoteAddress;
}
public final void setRemoteAddress(final String remoteAddress) {
this.remoteAddress = remoteAddress;
}
public final int getRemotePort() {
return remotePort;
}
public final void setRemotePort(final int remotePort) {
this.remotePort = remotePort;
}
public final String getStatus() {
return status;
}
public final void setStatus(final String status) {
this.status = status;
}
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("[Prot=").append(getProtocolAsString());
buf.append(",POwner=").append(powner);
buf.append(",PID=").append(pid);
buf.append(",PName=").append(pname);
buf.append(",LPort=").append(localPort);
buf.append(",RAddress=").append(remoteAddress);
buf.append(",RPort=").append(remotePort);
buf.append(",Status=").append(status);
buf.append("]");
return buf.toString();
}
}
}

View File

@ -16,11 +16,11 @@
package org.fdroid.fdroid;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import androidx.test.uiautomator.UiWatcher;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
import android.support.test.uiautomator.UiWatcher;
import android.util.Log;
import java.util.ArrayList;
@ -117,7 +117,7 @@ public class UiWatchers {
return false; // no trigger
}
});
Log.i(LOG_TAG, "Registered GUI Exception watchers");
Log.i(LOG_TAG, "Registed GUI Exception watchers");
}
public void onAnrDetected(String errorText) {

View File

@ -4,8 +4,8 @@ import android.app.Instrumentation;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import org.fdroid.fdroid.AssetUtils;

View File

@ -22,10 +22,10 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.fdroid.fdroid.AssetUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest;
@ -113,7 +113,7 @@ public class ApkVerifierTest {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
ArrayList<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList(
String[] noPrefixPermissions = new String[]{
"AUTHENTICATE_ACCOUNTS",
"MANAGE_ACCOUNTS",
"READ_PROFILE",
@ -129,13 +129,8 @@ public class ApkVerifierTest {
"READ_SYNC_SETTINGS",
"WRITE_SYNC_SETTINGS",
"WRITE_CALL_LOG", // implied-permission!
"READ_CALL_LOG" // implied-permission!
));
if (Build.VERSION.SDK_INT >= 29) {
noPrefixPermissionsList.add("android.permission.ACCESS_MEDIA_LOCATION");
}
String[] noPrefixPermissions = noPrefixPermissionsList.toArray(new String[0]);
"READ_CALL_LOG", // implied-permission!
};
for (int i = 0; i < noPrefixPermissions.length; i++) {
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
}
@ -182,7 +177,7 @@ public class ApkVerifierTest {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
apk.requestedPermissions = new String[]{
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE",
@ -198,12 +193,8 @@ public class ApkVerifierTest {
"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]);
"android.permission.READ_CALL_LOG", // implied-permission!
};
Uri uri = Uri.fromFile(sdk14Apk);
@ -380,9 +371,6 @@ public class ApkVerifierTest {
"android.permission.MANAGE_ACCOUNTS"
));
}
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
Apk apk = actualDetails.apks.get(1);
Log.i(TAG, "APK: " + apk.apkName);
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
@ -419,9 +407,6 @@ public class ApkVerifierTest {
"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 = actualDetails.apks.get(2);
Log.i(TAG, "APK: " + apk.apkName);

View File

@ -1,128 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.fdroid.fdroid.FDroidApp;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceListener;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class BonjourManagerTest {
private static final String NAME = "Robolectric-test";
private static final String LOCALHOST = "localhost";
private static final int PORT = 8888;
@Test
public void testStartStop() throws InterruptedException {
Context context = ApplicationProvider.getApplicationContext();
FDroidApp.ipAddressString = LOCALHOST;
FDroidApp.port = PORT;
final CountDownLatch addedLatch = new CountDownLatch(1);
final CountDownLatch resolvedLatch = new CountDownLatch(1);
final CountDownLatch removedLatch = new CountDownLatch(1);
BonjourManager.start(context, NAME, false,
new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent serviceEvent) {
System.out.println("Service added: " + serviceEvent.getInfo());
if (NAME.equals(serviceEvent.getName())) {
addedLatch.countDown();
}
}
@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
System.out.println("Service removed: " + serviceEvent.getInfo());
removedLatch.countDown();
}
@Override
public void serviceResolved(ServiceEvent serviceEvent) {
System.out.println("Service resolved: " + serviceEvent.getInfo());
if (NAME.equals(serviceEvent.getName())) {
resolvedLatch.countDown();
}
}
}, getBlankServiceListener());
BonjourManager.setVisible(context, true);
assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
BonjourManager.setVisible(context, false);
assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
BonjourManager.stop(context);
}
@Test
public void testRestart() throws InterruptedException {
Context context = ApplicationProvider.getApplicationContext();
FDroidApp.ipAddressString = LOCALHOST;
FDroidApp.port = PORT;
BonjourManager.start(context, NAME, false, getBlankServiceListener(), getBlankServiceListener());
final CountDownLatch addedLatch = new CountDownLatch(1);
final CountDownLatch resolvedLatch = new CountDownLatch(1);
final CountDownLatch removedLatch = new CountDownLatch(1);
BonjourManager.restart(context, NAME, false,
new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent serviceEvent) {
System.out.println("Service added: " + serviceEvent.getInfo());
if (NAME.equals(serviceEvent.getName())) {
addedLatch.countDown();
}
}
@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
System.out.println("Service removed: " + serviceEvent.getInfo());
removedLatch.countDown();
}
@Override
public void serviceResolved(ServiceEvent serviceEvent) {
System.out.println("Service resolved: " + serviceEvent.getInfo());
if (NAME.equals(serviceEvent.getName())) {
resolvedLatch.countDown();
}
}
}, getBlankServiceListener());
BonjourManager.setVisible(context, true);
assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
BonjourManager.setVisible(context, false);
assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
BonjourManager.stop(context);
}
private ServiceListener getBlankServiceListener() {
return new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent serviceEvent) {
}
@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
}
@Override
public void serviceResolved(ServiceEvent serviceEvent) {
}
};
}
}

View File

@ -1,192 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Netstat;
import org.fdroid.fdroid.Utils;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class LocalHTTPDManagerTest {
private static final String TAG = "LocalHTTPDManagerTest";
private Context context;
private LocalBroadcastManager lbm;
private static final String LOCALHOST = "localhost";
private static final int PORT = 8888;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
lbm = LocalBroadcastManager.getInstance(context);
FDroidApp.ipAddressString = LOCALHOST;
FDroidApp.port = PORT;
for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
}
assertFalse(Utils.isServerSocketInUse(PORT));
LocalHTTPDManager.stop(context);
for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
}
}
@After
public void tearDown() {
lbm.unregisterReceiver(startedReceiver);
lbm.unregisterReceiver(stoppedReceiver);
lbm.unregisterReceiver(errorReceiver);
}
@Ignore
@Test
public void testStartStop() throws InterruptedException {
Log.i(TAG, "testStartStop");
final CountDownLatch startLatch = new CountDownLatch(1);
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
startLatch.countDown();
}
};
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
LocalHTTPDManager.start(context, false);
assertTrue(startLatch.await(30, TimeUnit.SECONDS));
assertTrue(Utils.isServerSocketInUse(PORT));
assertTrue(Utils.canConnectToSocket(LOCALHOST, PORT));
lbm.unregisterReceiver(latchReceiver);
lbm.unregisterReceiver(stoppedReceiver);
lbm.unregisterReceiver(errorReceiver);
final CountDownLatch stopLatch = new CountDownLatch(1);
latchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
stopLatch.countDown();
}
};
lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
LocalHTTPDManager.stop(context);
assertTrue(stopLatch.await(30, TimeUnit.SECONDS));
assertFalse(Utils.isServerSocketInUse(PORT));
assertFalse(Utils.canConnectToSocket(LOCALHOST, PORT)); // if this is flaky, just remove it
lbm.unregisterReceiver(latchReceiver);
}
@Test
public void testError() throws InterruptedException, IOException {
Log.i("LocalHTTPDManagerTest", "testError");
ServerSocket blockerSocket = new ServerSocket(PORT);
final CountDownLatch latch = new CountDownLatch(1);
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
latch.countDown();
}
};
lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
LocalHTTPDManager.start(context, false);
assertTrue(latch.await(30, TimeUnit.SECONDS));
assertTrue(Utils.isServerSocketInUse(PORT));
assertNotEquals(PORT, FDroidApp.port);
assertFalse(Utils.isServerSocketInUse(FDroidApp.port));
lbm.unregisterReceiver(latchReceiver);
blockerSocket.close();
}
@Test
public void testRestart() throws InterruptedException, IOException {
Log.i("LocalHTTPDManagerTest", "testRestart");
assertFalse(Utils.isServerSocketInUse(PORT));
final CountDownLatch startLatch = new CountDownLatch(1);
BroadcastReceiver latchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
startLatch.countDown();
}
};
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
LocalHTTPDManager.start(context, false);
assertTrue(startLatch.await(30, TimeUnit.SECONDS));
assertTrue(Utils.isServerSocketInUse(PORT));
lbm.unregisterReceiver(latchReceiver);
lbm.unregisterReceiver(stoppedReceiver);
final CountDownLatch restartLatch = new CountDownLatch(1);
latchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
restartLatch.countDown();
}
};
lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
LocalHTTPDManager.restart(context, false);
assertTrue(restartLatch.await(30, TimeUnit.SECONDS));
lbm.unregisterReceiver(latchReceiver);
}
private final BroadcastReceiver startedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
Log.i(TAG, "startedReceiver: " + message);
fail();
}
};
private final BroadcastReceiver stoppedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
Log.i(TAG, "stoppedReceiver: " + message);
fail();
}
};
private final BroadcastReceiver errorReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
Log.i(TAG, "errorReceiver: " + message);
fail();
}
};
}

View File

@ -70,7 +70,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() {
@Override
public void onProgress(long bytesRead, long totalBytes) {
public void onProgress(String urlString, long bytesRead, long totalBytes) {
receivedProgress = true;
}
});
@ -132,7 +132,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() {
@Override
public void onProgress(long bytesRead, long totalBytes) {
public void onProgress(String urlString, long bytesRead, long totalBytes) {
receivedProgress = true;
latch.countDown();
}

View File

@ -1,203 +0,0 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.os.Looper;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.nearby.LocalHTTPD;
import org.fdroid.fdroid.nearby.LocalRepoKeyStore;
import org.fdroid.fdroid.nearby.LocalRepoManager;
import org.fdroid.fdroid.nearby.LocalRepoService;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.security.cert.Certificate;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@LargeTest
public class SwapRepoEmulatorTest {
public static final String TAG = "SwapRepoEmulatorTest";
/**
* @see org.fdroid.fdroid.nearby.WifiStateChangeService.WifiInfoThread#run()
*/
@Ignore
@Test
public void testSwap()
throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
Looper.prepare();
LocalHTTPD localHttpd = null;
try {
Log.i(TAG, "REPO: " + FDroidApp.repo);
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
Preferences.setupForTests(context);
FDroidApp.initWifiSettings();
assertNull(FDroidApp.repo.address);
final CountDownLatch latch = new CountDownLatch(1);
new Thread() {
@Override
public void run() {
while (FDroidApp.repo.address == null) {
try {
Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address);
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignored
}
}
latch.countDown();
}
}.start();
latch.await(10, TimeUnit.MINUTES);
assertNotNull(FDroidApp.repo.address);
LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
Log.i(TAG, "REPO: " + FDroidApp.repo);
File indexJarFile = LocalRepoManager.get(context).getIndexJar();
assertTrue(indexJarFile.isFile());
localHttpd = new LocalHTTPD(
context,
null,
FDroidApp.port,
LocalRepoManager.get(context).getWebRoot(),
false);
localHttpd.start();
Thread.sleep(100); // give the server some tine to start.
assertTrue(localHttpd.isAlive());
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
Certificate localCert = localRepoKeyStore.getCertificate();
String signingCert = Hasher.hex(localCert);
assertFalse(TextUtils.isEmpty(signingCert));
assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
while (repoToDelete != null) {
Log.d(TAG, "Removing old test swap repo matching this one: " + repoToDelete.address);
RepoProvider.Helper.remove(context, repoToDelete.getId());
repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
}
ContentValues values = new ContentValues(4);
values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.address);
values.put(Schema.RepoTable.Cols.NAME, FDroidApp.repo.name);
values.put(Schema.RepoTable.Cols.IS_SWAP, true);
final String lastEtag = UUID.randomUUID().toString();
values.put(Schema.RepoTable.Cols.LAST_ETAG, lastEtag);
RepoProvider.Helper.insert(context, values);
Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
assertTrue(repo.isSwap);
assertNotEquals(-1, repo.getId());
assertTrue(repo.name.startsWith(FDroidApp.repo.name));
assertEquals(lastEtag, repo.lastetag);
assertNull(repo.lastUpdated);
assertTrue(isPortInUse(FDroidApp.ipAddressString, FDroidApp.port));
Thread.sleep(100);
IndexUpdater updater = new IndexUpdater(context, repo);
updater.update();
assertTrue(updater.hasChanged());
repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
final Date lastUpdated = repo.lastUpdated;
assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0);
App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(),
context.getPackageName(), repo.getId());
assertEquals(context.getPackageName(), app.packageName);
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
assertEquals(1, apks.size());
for (Apk apk : apks) {
Log.i(TAG, "Apk: " + apk);
assertEquals(context.getPackageName(), apk.packageName);
assertEquals(BuildConfig.VERSION_NAME, apk.versionName);
assertEquals(BuildConfig.VERSION_CODE, apk.versionCode);
assertEquals(app.repoId, apk.repoId);
}
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
HashSet<String> packageNames = new HashSet<>();
for (ResolveInfo resolveInfo : resolveInfoList) {
if (!isSystemPackage(resolveInfo)) {
Log.i(TAG, "resolveInfo: " + resolveInfo);
packageNames.add(resolveInfo.activityInfo.packageName);
}
}
LocalRepoService.runProcess(context, packageNames.toArray(new String[0]));
updater = new IndexUpdater(context, repo);
updater.update();
assertTrue(updater.hasChanged());
assertTrue("repo lastUpdated should be updated", lastUpdated.compareTo(repo.lastUpdated) < 0);
for (String packageName : packageNames) {
assertNotNull(ApkProvider.Helper.findByPackageName(context, packageName));
}
} finally {
if (localHttpd != null) {
localHttpd.stop();
}
}
if (localHttpd != null) {
assertFalse(localHttpd.isAlive());
}
}
private boolean isPortInUse(String host, int port) {
boolean result = false;
try {
(new Socket(host, port)).close();
result = true;
} catch (IOException e) {
// Could not connect.
e.printStackTrace();
}
return result;
}
private boolean isSystemPackage(ResolveInfo resolveInfo) {
return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
}

View File

@ -1,110 +0,0 @@
package org.fdroid.fdroid.work;
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* 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,72 +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 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;
import static org.junit.Assert.assertEquals;
/**
* 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,33 +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 {
Context targetContext;
Context testContext;
Configuration configuration;
WorkManager workManager;
@Override
protected void starting(Description description) {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
targetContext = instrumentation.getTargetContext();
testContext = instrumentation.getContext();
configuration = new Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(new SynchronousExecutor())
.build();
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration);
workManager = WorkManager.getInstance(targetContext);
}
}

View File

@ -1,7 +1,3 @@
-dontoptimize
-dontwarn
-dontobfuscate
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontnote junit.framework.**
@ -18,8 +14,3 @@
-keep class junit.** { *; }
-dontwarn junit.**
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
public <init>(...);
}

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.content.Context;

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby.peers;
package org.fdroid.fdroid.localrepo.peers;
import org.fdroid.fdroid.data.NewRepoConfig;

View File

@ -1,5 +0,0 @@
package org.fdroid.fdroid.nearby;
public class LocalRepoManager {
public static final String[] WEB_ROOT_ASSET_FILES = {};
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2018 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.nearby;
import android.content.Context;
/**
* Dummy version for basic app flavor.
*/
public class SwapService {
public static void start(Context context) {
}
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2018 Senecto Limited
*
* 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.nearby;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
/**
* Dummy version for basic app flavor.
*/
public class TreeUriScannerIntentService {
public static void onActivityResult(AppCompatActivity activity, Intent intent) {
throw new IllegalStateException("unimplemented");
}
}

View File

@ -17,11 +17,11 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.net;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import android.support.annotation.Nullable;
/**
* Dummy version for basic app flavor.

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.net.bluetooth;
/**
* Dummy version for basic app flavor.

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.panic;
package org.fdroid.fdroid.views.hiding;
import android.content.Context;

View File

@ -19,11 +19,11 @@
package org.fdroid.fdroid.views.main;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.PreferencesFragment;
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
@ -49,10 +49,10 @@ class MainViewController extends RecyclerView.ViewHolder {
}
/**
* @see LatestViewBinder
* @see WhatsNewViewBinder
*/
public void bindLatestView() {
new LatestViewBinder(activity, frame);
public void bindWhatsNewView() {
new WhatsNewViewBinder(activity, frame);
}
/**

View File

@ -1,9 +1,10 @@
package org.fdroid.fdroid.views.main;
import android.content.Context;
import android.app.Activity;
import android.content.Intent;
class NearbyViewBinder {
public static void updateUsbOtg(Context context) {
static void onActivityResult(Activity activity, Intent data) {
throw new IllegalStateException("unimplemented");
}
}

View File

@ -17,18 +17,12 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
import android.content.Context;
import android.net.Uri;
package org.fdroid.fdroid.views.swap;
/**
* Dummy version for basic app flavor.
*/
public class SwapWorkflowActivity {
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
public static void requestSwap(Context context, Uri uri) {
}
public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM";
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -1,16 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MenuTitle">
<!-- android:title and android:icon are set dynamically in MainActivity -->
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/main_menu__latest_apps"
android:icon="@drawable/ic_latest"
app:showAsAction="ifRoom|withText"
android:id="@+id/latest"/>
android:id="@+id/whats_new" />
<item
android:title="@string/updates"
android:icon="@drawable/ic_updates"
app:showAsAction="ifRoom|withText"
android:id="@+id/updates"/>
android:id="@+id/updates" />
<item
android:title="@string/menu_settings"
android:icon="@drawable/ic_settings"
app:showAsAction="ifRoom|withText"
android:id="@+id/settings"/>
android:id="@+id/settings" />
</menu>

View File

@ -1,25 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen android:title="@string/about_title"
android:key="pref_about" />
<android.support.v7.preference.PreferenceScreen android:title="@string/about_title">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="@string/applicationId"
android:targetClass="org.fdroid.fdroid.AboutActivity"/>
</android.support.v7.preference.PreferenceScreen>
<PreferenceCategory android:title="@string/preference_category__my_apps">
<PreferenceScreen android:title="@string/preference_manage_installed_apps">
<android.support.v7.preference.PreferenceCategory android:title="@string/preference_category__my_apps">
<android.support.v7.preference.PreferenceScreen android:title="@string/preference_manage_installed_apps">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="@string/applicationId"
android:targetClass="org.fdroid.fdroid.views.installed.InstalledAppsActivity"/>
</PreferenceScreen>
<PreferenceScreen
</android.support.v7.preference.PreferenceScreen>
<android.support.v7.preference.PreferenceScreen
android:title="@string/menu_manage"
android:summary="@string/repositories_summary">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="@string/applicationId"
android:targetClass="org.fdroid.fdroid.views.ManageReposActivity"/>
</PreferenceScreen>
<PreferenceScreen
</android.support.v7.preference.PreferenceScreen>
<android.support.v7.preference.PreferenceScreen
android:key="installHistory"
android:visible="false"
android:title="@string/install_history"
@ -28,10 +33,10 @@
android:action="android.intent.action.MAIN"
android:targetPackage="@string/applicationId"
android:targetClass="org.fdroid.fdroid.views.InstallHistoryActivity"/>
</PreferenceScreen>
</PreferenceCategory>
</android.support.v7.preference.PreferenceScreen>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/updates">
<android.support.v7.preference.PreferenceCategory android:title="@string/updates">
<org.fdroid.fdroid.views.LiveSeekBarPreference
android:key="overWifi"
android:title="@string/over_wifi"
@ -42,22 +47,23 @@
android:title="@string/over_data"
android:defaultValue="@integer/defaultOverData"
android:layout="@layout/preference_seekbar"/>
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/update_auto_download"
android:summary="@string/update_auto_download_summary"
android:defaultValue="true"
android:key="updateAutoDownload"/>
<org.fdroid.fdroid.views.LiveSeekBarPreference
android:key="updateIntervalSeekBarPosition"
android:title="@string/update_interval"
android:defaultValue="@integer/defaultUpdateInterval"
android:layout="@layout/preference_seekbar"/>
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/notify"
android:defaultValue="true"
android:key="updateNotify"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/display"
<android.support.v7.preference.PreferenceCategory android:title="@string/display"
android:key="pref_category_display">
<ListPreference
android:title="@string/pref_language"
@ -68,30 +74,30 @@
android:defaultValue="light"
android:entries="@array/themeNames"
android:entryValues="@array/themeValues"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/appcompatibility"
<android.support.v7.preference.PreferenceCategory android:title="@string/appcompatibility"
android:key="pref_category_appcompatibility">
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/show_incompat_versions"
android:defaultValue="false"
android:key="incompatibleVersions"/>
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/show_anti_feature_apps"
android:defaultValue="false"
android:key="showAntiFeatureApps"/>
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/force_touch_apps"
android:defaultValue="false"
android:key="ignoreTouchscreen"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/proxy">
<SwitchPreferenceCompat
<android.support.v7.preference.PreferenceCategory android:title="@string/proxy">
<SwitchPreference
android:key="useTor"
android:summary="@string/useTorSummary"
android:title="@string/useTor"/>
<SwitchPreferenceCompat
<SwitchPreference
android:defaultValue="false"
android:key="enableProxy"
android:title="@string/enable_proxy_title"
@ -106,24 +112,24 @@
android:title="@string/proxy_port"
android:summary="@string/proxy_port_summary"
android:dependency="enableProxy"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory
<android.support.v7.preference.PreferenceCategory
android:key="pref_category_privacy"
android:title="@string/privacy">
<SwitchPreferenceCompat
<SwitchPreference
android:key="promptToSendCrashReports"
android:title="@string/prompt_to_send_crash_reports"
android:summary="@string/prompt_to_send_crash_reports_summary"
android:defaultValue="true"/>
<SwitchPreferenceCompat
<SwitchPreference
android:defaultValue="false"
android:key="preventScreenshots"
android:summary="@string/preventScreenshots_summary"
android:title="@string/preventScreenshots_title"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
<PreferenceCategory
<android.support.v7.preference.PreferenceCategory
android:title="@string/other"
android:key="pref_category_other">
<ListPreference
@ -132,7 +138,7 @@
android:defaultValue="86400000"
android:entries="@array/keepCacheNames"
android:entryValues="@array/keepCacheValues"/>
<SwitchPreferenceCompat
<SwitchPreference
android:title="@string/expert"
android:defaultValue="false"
android:key="expert"/>
@ -148,12 +154,6 @@
android:summary="@string/keep_install_history_summary"
android:defaultValue="false"
android:dependency="expert"/>
<CheckBoxPreference
android:key="sendToFdroidMetrics"
android:title="@string/send_to_fdroid_metrics"
android:summary="@string/send_to_fdroid_metrics_summary"
android:defaultValue="false"
android:dependency="expert"/>
<CheckBoxPreference
android:key="hideAllNotifications"
android:title="@string/hide_all_notifications"
@ -178,6 +178,6 @@
android:key="privilegedInstaller"
android:persistent="false"
android:dependency="expert"/>
</PreferenceCategory>
</android.support.v7.preference.PreferenceCategory>
</PreferenceScreen>
</android.support.v7.preference.PreferenceScreen>

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This file should be outside of release manifest (in this case app/src/mock/Manifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--required to enable/disable system animations from the app itself during Espresso test runs-->
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>
</manifest>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright (C) 2010-2012 Ciaran Gultnieks
* Copyright (C) 2013-2017 Peter Serwylo
@ -27,152 +26,109 @@
package="org.fdroid.fdroid"
android:installLocation="auto">
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission
android:name="android.permission.USB_PERMISSION"
android:maxSdkVersion="22" /><!-- maybe unnecessary -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application>
<activity
android:name=".nearby.SwapWorkflowActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/swap"
android:launchMode="singleTask"
android:name=".views.swap.SwapWorkflowActivity"
android:parentActivityName=".views.main.MainActivity"
android:screenOrientation="portrait">
android:theme="@style/SwapTheme.Wizard"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".views.main.MainActivity" />
android:value=".views.main.MainActivity"/>
</activity>
<receiver android:name=".receiver.WifiStateChangeReceiver">
<intent-filter>
<action android:name="android.net.wifi.STATE_CHANGE"/>
</intent-filter>
</receiver>
<receiver android:name=".receiver.DeviceStorageReceiver">
<intent-filter>
<action android:name="android.intent.action.DEVICE_STORAGE_LOW"/>
</intent-filter>
</receiver>
<service
android:name=".net.WifiStateChangeService"
android:exported="false"/>
<service android:name=".localrepo.SwapService"/>
<service
android:name=".localrepo.CacheSwapAppsService"
android:exported="false"/>
<service
android:name=".localrepo.TreeUriScannerIntentService"
android:exported="false"/>
<service
android:name=".localrepo.SDCardScannerService"
android:exported="false"/>
<activity
android:name=".panic.PanicPreferencesActivity"
android:name=".views.panic.PanicPreferencesActivity"
android:label="@string/panic_settings"
android:parentActivityName=".views.main.MainActivity">
<intent-filter>
<action android:name="info.guardianproject.panic.action.CONNECT" />
<action android:name="info.guardianproject.panic.action.DISCONNECT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".views.main.MainActivity" />
android:value=".views.main.MainActivity"/>
<intent-filter>
<action android:name="info.guardianproject.panic.action.CONNECT"/>
<action android:name="info.guardianproject.panic.action.DISCONNECT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".panic.SelectInstalledAppsActivity"
android:parentActivityName=".panic.PanicPreferencesActivity" />
<activity
android:name=".panic.PanicResponderActivity"
android:name=".views.panic.PanicResponderActivity"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<!-- this can never have launchMode singleTask or singleInstance! -->
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".panic.ExitActivity"
android:theme="@android:style/Theme.NoDisplay" />
android:name=".views.panic.ExitActivity"
android:theme="@android:style/Theme.NoDisplay"/>
<activity
android:name=".panic.CalculatorActivity"
android:name=".views.hiding.CalculatorActivity"
android:enabled="false"
android:icon="@mipmap/ic_calculator_launcher"
android:label="@string/hiding_calculator">
android:label="@string/hiding_calculator"
android:theme="@style/AppThemeLight">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name=".nearby.WifiStateChangeReceiver">
<intent-filter>
<action android:name="android.net.wifi.STATE_CHANGE" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.DeviceStorageReceiver">
<intent-filter>
<action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
</intent-filter>
</receiver>
<receiver android:name=".nearby.UsbDeviceAttachedReceiver">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</receiver>
<receiver android:name=".nearby.UsbDeviceDetachedReceiver">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
android:resource="@xml/device_filter" />
</receiver>
<receiver android:name=".nearby.UsbDeviceMediaMountedReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_EJECT" />
<action android:name="android.intent.action.MEDIA_REMOVED" />
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<action android:name="android.intent.action.MEDIA_BAD_REMOVAL" />
<data android:scheme="content" />
<data android:scheme="file" />
</intent-filter>
</receiver>
<service
android:name=".nearby.WifiStateChangeService"
android:exported="false" />
<service android:name=".nearby.SwapService" />
<service
android:name=".nearby.LocalRepoService"
android:exported="false" />
<service
android:name=".nearby.TreeUriScannerIntentService"
android:exported="false" />
<service
android:name=".nearby.SDCardScannerService"
android:exported="false" />
</application>

View File

@ -4,13 +4,12 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.util.ByteWrangler;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.util.ByteWrangler;
/**
* The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
* In order to make it Parcelable (or Serializable for that matter), there are some package-scope

View File

@ -113,7 +113,7 @@ public class KeyStoreFileManager {
File keystoreFile = new File(keystorePath);
try {
if (keystoreFile.exists()) {
// I've had some trouble saving new versions of the keystore file in which the file becomes empty/corrupt.
// I've had some trouble saving new verisons of the keystore file in which the file becomes empty/corrupt.
// Saving the new version to a new file and creating a backup of the old version.
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
FileOutputStream fos = new FileOutputStream(tmpFile);

View File

@ -0,0 +1,85 @@
package org.fdroid.fdroid.localrepo;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import java.io.File;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
/**
* An {@link IntentService} subclass for generating cached info about the installed APKs
* which are available for swapping. It does not cache system apps, since those are
* rarely swapped. This is meant to start running when {@link SwapService} starts.
* <p>
* This could probably be replaced by {@link org.fdroid.fdroid.data.InstalledAppProvider}
* if that contained all of the info to generate complete {@link App} and
* {@link org.fdroid.fdroid.data.Apk} instances.
*/
public class CacheSwapAppsService extends IntentService {
private static final String TAG = "CacheSwapAppsService";
private static final String ACTION_PARSE_APP = "org.fdroid.fdroid.localrepo.action.PARSE_APP";
public CacheSwapAppsService() {
super("CacheSwapAppsService");
}
/**
* Parse the locally installed APK for {@code packageName} and save its XML
* to the APK XML cache.
*/
private static void parseApp(Context context, String packageName) {
Intent intent = new Intent();
intent.setData(Utils.getPackageUri(packageName));
intent.setClass(context, CacheSwapAppsService.class);
intent.setAction(ACTION_PARSE_APP);
context.startService(intent);
}
/**
* Parse all of the locally installed APKs into a memory cache, starting
* with the currently selected apps. APKs that are already parsed in the
* {@code index.jar} file will be read from that file.
*/
public static void startCaching(Context context) {
File indexJarFile = LocalRepoManager.get(context).getIndexJar();
PackageManager pm = context.getPackageManager();
for (ApplicationInfo applicationInfo : pm.getInstalledApplications(0)) {
if (applicationInfo.publicSourceDir.startsWith(FDroidApp.SYSTEM_DIR_NAME)) {
continue;
}
if (!indexJarFile.exists()
|| FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) {
parseApp(context, applicationInfo.packageName);
}
}
}
@Override
protected void onHandleIntent(Intent intent) {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
if (intent == null || !ACTION_PARSE_APP.equals(intent.getAction())) {
Utils.debugLog(TAG, "received bad Intent: " + intent);
return;
}
try {
PackageManager pm = getPackageManager();
String packageName = intent.getData().getSchemeSpecificPart();
App app = App.getInstance(this, pm, packageName);
if (app != null) {
SwapService.putAppInCache(packageName, app);
}
} catch (CertificateEncodingException | IOException | PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}

View File

@ -1,8 +1,10 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.content.Context;
import android.util.Log;
import kellinwood.security.zipsigner.ZipSigner;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.GeneralName;
@ -16,9 +18,10 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -46,12 +49,6 @@ import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;
import kellinwood.security.zipsigner.ZipSigner;
// TODO Address exception handling in a uniform way throughout
@SuppressWarnings("LineLength")

View File

@ -1,4 +1,4 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@ -10,9 +10,10 @@ import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.IndexUpdater;
@ -20,8 +21,6 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.InstalledApp;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.SanitizedFile;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@ -42,22 +41,20 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* The {@link SwapService} deals with managing the entire workflow from selecting apps to
* swap, to invoking this class to prepare the webroot, to enabling various communication protocols.
* This class deals specifically with the webroot side of things, ensuring we have a valid index.jar
* and the relevant .apk and icon files available.
*/
@SuppressWarnings("LineLength")
public final class LocalRepoManager {
private static final String TAG = "LocalRepoManager";
@ -66,13 +63,13 @@ public final class LocalRepoManager {
private final AssetManager assetManager;
private final String fdroidPackageName;
public static final String[] WEB_ROOT_ASSET_FILES = {
private static final String[] WEB_ROOT_ASSET_FILES = {
"swap-icon.png",
"swap-tick-done.png",
"swap-tick-not-done.png",
};
private final Map<String, App> apps = new ConcurrentHashMap<>();
private final Map<String, App> apps = new HashMap<>();
private final SanitizedFile xmlIndexJar;
private final SanitizedFile xmlIndexJarUnsigned;
@ -125,7 +122,7 @@ public final class LocalRepoManager {
private String writeFdroidApkToWebroot() {
ApplicationInfo appInfo;
String fdroidClientURL = "https://f-droid.org/F-Droid.apk";
String fdroidClientURL = "https://f-droid.org/FDroid.apk";
try {
appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA);
@ -249,10 +246,6 @@ public final class LocalRepoManager {
return xmlIndexJar;
}
public File getWebRoot() {
return webRoot;
}
public void deleteRepo() {
deleteContents(repoDir);
}
@ -277,10 +270,12 @@ public final class LocalRepoManager {
}
public void addApp(Context context, String packageName) {
App app = null;
App app;
try {
InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName);
app = App.getInstance(context, pm, installedApp, packageName);
app = SwapService.getAppFromCache(packageName);
if (app == null) {
app = App.getInstance(context.getApplicationContext(), pm, packageName);
}
if (app == null || !app.isValid()) {
return;
}
@ -349,8 +344,7 @@ public final class LocalRepoManager {
serializer = XmlPullParserFactory.newInstance().newSerializer();
}
public void build(Context context, Map<String, App> apps, OutputStream output)
throws IOException, LocalRepoKeyStore.InitException {
public void build(Context context, Map<String, App> apps, OutputStream output) throws IOException, LocalRepoKeyStore.InitException {
serializer.setOutput(output, "UTF-8");
serializer.startDocument(null, null);
serializer.startTag("", "fdroid");
@ -358,14 +352,12 @@ public final class LocalRepoManager {
// <repo> block
serializer.startTag("", "repo");
serializer.attribute("", "icon", "blah.png");
serializer.attribute("", "name", Preferences.get().getLocalRepoName()
+ " on " + FDroidApp.ipAddressString);
serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString);
serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
long timestamp = System.currentTimeMillis() / 1000L;
serializer.attribute("", "timestamp", String.valueOf(timestamp));
serializer.attribute("", "version", "10");
tag("description", "A local FDroid repo generated from apps installed on "
+ Preferences.get().getLocalRepoName());
tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName());
serializer.endTag("", "repo");
// <application> blocks
@ -390,7 +382,7 @@ public final class LocalRepoManager {
}
/**
* Alias for {@link org.fdroid.fdroid.nearby.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
* Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
* That accepts a number instead of string.
*
* @see IndexXmlBuilder#tag(String, String)
@ -400,7 +392,7 @@ public final class LocalRepoManager {
}
/**
* Alias for {@link org.fdroid.fdroid.nearby.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
* Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
* that accepts a date instead of a string.
*
* @see IndexXmlBuilder#tag(String, String)
@ -418,7 +410,7 @@ public final class LocalRepoManager {
tag("lastupdated", app.lastUpdated);
tag("name", app.name);
tag("summary", app.summary);
tag("icon", app.iconFromApk);
tag("icon", app.icon);
tag("desc", app.description);
tag("license", "Unknown");
tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName());

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.Manifest;
import android.app.IntentService;
@ -28,10 +28,11 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Process;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import java.io.File;
@ -44,11 +45,9 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import androidx.core.content.ContextCompat;
/**
* An {@link IntentService} subclass for scanning removable "external storage"
* for F-Droid package repos, e.g. SD Cards. This is intended to support
* for F-Droid package repos, e.g. SD Cards. This is intented to support
* sharable package repos, so it ignores non-removable storage, like the fake
* emulated sdcard from devices with only built-in storage. This method will
* only ever allow for reading repos, never writing. It also will not work
@ -56,7 +55,7 @@ import androidx.core.content.ContextCompat;
* "External Storage"
* <p>
* Scanning the removable storage requires that the user allowed it. This
* requires both the {@link org.fdroid.fdroid.Preferences#isScanRemovableStorageEnabled()}
* requires both the {@link Preferences#isScanRemovableStorageEnabled()}
* and the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}
* permission to be enabled.
*
@ -67,7 +66,7 @@ import androidx.core.content.ContextCompat;
public class SDCardScannerService extends IntentService {
public static final String TAG = "SDCardScannerService";
private static final String ACTION_SCAN = "org.fdroid.fdroid.nearby.SCAN";
private static final String ACTION_SCAN = "org.fdroid.fdroid.localrepo.SCAN";
private static final List<String> SKIP_DIRS = Arrays.asList(".android_secure", "LOST.DIR");
@ -76,10 +75,12 @@ public class SDCardScannerService extends IntentService {
}
public static void scan(Context context) {
if (Preferences.get().isScanRemovableStorageEnabled()) {
Intent intent = new Intent(context, SDCardScannerService.class);
intent.setAction(ACTION_SCAN);
context.startService(intent);
}
}
@Override
protected void onHandleIntent(Intent intent) {

View File

@ -1,5 +1,6 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
@ -12,31 +13,41 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.IBinder;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NotificationHelper;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.nearby.peers.Peer;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.localrepo.peers.PeerFinder;
import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
import org.fdroid.fdroid.localrepo.type.SwapType;
import org.fdroid.fdroid.localrepo.type.WifiSwap;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
@ -44,73 +55,188 @@ import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import cc.mvdan.accesspoint.WifiApControl;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.util.concurrent.ConcurrentHashMap;
/**
* Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps.
*/
@SuppressWarnings("LineLength")
public class SwapService extends Service {
private static final String TAG = "SwapService";
private static final String SHARED_PREFERENCES = "swap-state";
private static final String KEY_APPS_TO_SWAP = "appsToSwap";
private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled";
private static final String KEY_WIFI_ENABLED = "wifiEnabled";
private static final String KEY_HOTSPOT_ACTIVATED = "hotspotEnabled";
private static final String KEY_BLUETOOTH_ENABLED_BEFORE_SWAP = "bluetoothEnabledBeforeSwap";
private static final String KEY_BLUETOOTH_NAME_BEFORE_SWAP = "bluetoothNameBeforeSwap";
private static final String KEY_WIFI_ENABLED_BEFORE_SWAP = "wifiEnabledBeforeSwap";
private static final String KEY_HOTSPOT_ACTIVATED_BEFORE_SWAP = "hotspotEnabledBeforeSwap";
@NonNull
private final Set<String> appsToSwap = new HashSet<>();
private final Set<Peer> activePeers = new HashSet<>();
private static LocalBroadcastManager localBroadcastManager;
/**
* A cache of parsed APKs from the file system.
*/
private static final ConcurrentHashMap<String, App> INSTALLED_APPS = new ConcurrentHashMap<>();
private static SharedPreferences swapPreferences;
private static BluetoothAdapter bluetoothAdapter;
private static WifiManager wifiManager;
private static Timer pollConnectedSwapRepoTimer;
public static void stop(Context context) {
Intent intent = new Intent(context, SwapService.class);
context.stopService(intent);
}
static App getAppFromCache(String packageName) {
return INSTALLED_APPS.get(packageName);
}
static void putAppInCache(String packageName, @NonNull App app) {
INSTALLED_APPS.put(packageName, app);
}
// ==========================================================
// Search for peers to swap
// ==========================================================
private Observable<Peer> peerFinder;
/**
* Call {@link Observable#subscribe()} on this in order to be notified of peers
* which are found. Call {@link Subscription#unsubscribe()} on the resulting
* subscription when finished and you no longer want to scan for peers.
* <p>
* The returned object will scan for peers on a background thread, and emit
* found peers on the mian thread.
* <p>
* Invoking this in multiple places will return the same, cached, peer finder.
* That is, if in the past it already found some peers, then you subscribe
* to it in the future, the future subscriber will still receive the peers
* that were found previously.
* TODO: What about removing peers that no longer are present?
*/
public Observable<Peer> scanForPeers() {
Utils.debugLog(TAG, "Scanning for nearby devices to swap with...");
if (peerFinder == null) {
peerFinder = PeerFinder.createObservable(getApplicationContext())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.distinct();
}
return peerFinder;
}
// ==========================================================
// Manage the current step
// ("Step" refers to the current view being shown in the UI)
// ==========================================================
public static final int STEP_INTRO = 1;
public static final int STEP_SELECT_APPS = 2;
public static final int STEP_JOIN_WIFI = 3;
public static final int STEP_SHOW_NFC = 4;
public static final int STEP_WIFI_QR = 5;
public static final int STEP_CONNECTING = 6;
public static final int STEP_SUCCESS = 7;
public static final int STEP_CONFIRM_SWAP = 8;
/**
* Special view, that we don't really want to actually store against the
* {@link SwapService#step}. Rather, we use it for the purpose of specifying
* we are in the state waiting for the {@link SwapService} to get started and
* bound to the {@link SwapWorkflowActivity}.
*/
public static final int STEP_INITIAL_LOADING = 9;
@SwapStep
private int step = STEP_INTRO;
/**
* Current screen that the swap process is up to.
* Will be one of the SwapState.STEP_* values.
*/
@SwapStep
public int getStep() {
return step;
}
public SwapService setStep(@SwapStep int step) {
this.step = step;
return this;
}
@NonNull
public Set<String> getAppsToSwap() {
return appsToSwap;
}
@NonNull
public Set<Peer> getActivePeers() {
return activePeers;
public void refreshSwap() {
if (peer != null) {
connectTo(peer, false);
}
}
public void connectToPeer() {
if (getPeer() == null) {
throw new IllegalStateException("Cannot connect to peer, no peer has been selected.");
}
connectTo(getPeer());
if (LocalHTTPDManager.isAlive() && getPeer().shouldPromptForSwapBack()) {
askServerToSwapWithUs(peerRepo);
}
connectTo(getPeer(), getPeer().shouldPromptForSwapBack());
}
public void connectTo(@NonNull Peer peer) {
public void connectTo(@NonNull Peer peer, boolean requestSwapBack) {
if (peer != this.peer) {
Log.e(TAG, "Oops, got a different peer to swap with than initially planned.");
}
peerRepo = ensureRepoExists(peer);
// Only ask server to swap with us, if we are actually running a local repo service.
// It is possible to have a swap initiated without first starting a swap, in which
// case swapping back is pointless.
if (isEnabled() && requestSwapBack) {
askServerToSwapWithUs(peerRepo);
}
UpdateService.updateRepoNow(this, peer.getRepoAddress());
}
@SuppressLint("StaticFieldLeak")
private void askServerToSwapWithUs(final Repo repo) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... args) {
String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString();
HttpURLConnection conn = null;
try {
URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap"));
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
OutputStream outputStream = conn.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
writer.write("repo=" + swapBackUri);
writer.flush();
writer.close();
outputStream.close();
int responseCode = conn.getResponseCode();
Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " +
"POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode);
} catch (IOException e) {
Log.e(TAG, "Error while asking server to swap with us", e);
} finally {
conn.disconnect();
}
return null;
}
}.execute();
}
private Repo ensureRepoExists(@NonNull Peer peer) {
// TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with
// the repos address in the database. Not sure on best behaviour in this situation.
@ -143,6 +269,18 @@ public class SwapService extends Service {
return peerRepo;
}
/**
* Ensure that we don't get put into an incorrect state, by forcing people to pass valid
* states to setStep. Ideally this would be done by requiring an enum or something to
* be passed rather than in integer, however that is harder to persist on disk than an int.
* This is the same as, e.g. {@link Context#getSystemService(String)}
*/
@IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR,
STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP, STEP_INITIAL_LOADING})
@Retention(RetentionPolicy.SOURCE)
public @interface SwapStep {
}
// =================================================
// Have selected a specific peer to swap with
// (Rather than showing a generic QR code to scan)
@ -158,14 +296,6 @@ public class SwapService extends Service {
this.peer = peer;
}
public void addCurrentPeerToActive() {
activePeers.add(peer);
}
public void removeCurrentPeerFromActive() {
activePeers.remove(peer);
}
public boolean isConnectingWithPeer() {
return peer != null;
}
@ -252,14 +382,6 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED, visible).apply();
}
public static boolean getHotspotActivatedUserPreference() {
return swapPreferences.getBoolean(SwapService.KEY_HOTSPOT_ACTIVATED, false);
}
public static void putHotspotActivatedUserPreference(boolean visible) {
swapPreferences.edit().putBoolean(SwapService.KEY_HOTSPOT_ACTIVATED, visible).apply();
}
public static boolean wasBluetoothEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, false);
}
@ -268,14 +390,6 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, visible).apply();
}
public static String getBluetoothNameBeforeSwap() {
return swapPreferences.getString(SwapService.KEY_BLUETOOTH_NAME_BEFORE_SWAP, null);
}
public static void putBluetoothNameBeforeSwap(String name) {
swapPreferences.edit().putString(SwapService.KEY_BLUETOOTH_NAME_BEFORE_SWAP, name).apply();
}
public static boolean wasWifiEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, false);
}
@ -284,17 +398,60 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, visible).apply();
}
public static boolean wasHotspotEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_HOTSPOT_ACTIVATED_BEFORE_SWAP, false);
/**
* Handles checking if the {@link SwapService} is running, and only restarts it if it was running.
*/
public void stopWifiIfEnabled(final boolean restartAfterStopping) {
if (wifiSwap.isConnected()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Utils.debugLog(TAG, "Stopping the currently running WiFi swap service (on background thread)");
wifiSwap.stop();
if (restartAfterStopping) {
Utils.debugLog(TAG, "Restarting WiFi swap service after stopping (still on background thread)");
wifiSwap.start();
}
return null;
}
}.execute();
}
}
public static void putHotspotEnabledBeforeSwap(boolean visible) {
swapPreferences.edit().putBoolean(SwapService.KEY_HOTSPOT_ACTIVATED_BEFORE_SWAP, visible).apply();
public boolean isEnabled() {
return bluetoothSwap.isConnected() || wifiSwap.isConnected();
}
// ==========================================
// Interacting with Bluetooth adapter
// ==========================================
public boolean isBluetoothDiscoverable() {
return bluetoothSwap.isDiscoverable();
}
public boolean isBonjourDiscoverable() {
return wifiSwap.isConnected() && wifiSwap.getBonjour().isConnected();
}
// ===============================================================
// Old SwapService stuff being merged into that.
// ===============================================================
public static final String BONJOUR_STATE_CHANGE = "org.fdroid.fdroid.BONJOUR_STATE_CHANGE";
public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE";
public static final String WIFI_STATE_CHANGE = "org.fdroid.fdroid.WIFI_STATE_CHANGE";
public static final String EXTRA_STARTING = "STARTING";
public static final String EXTRA_STARTED = "STARTED";
public static final String EXTRA_STOPPING = "STOPPING";
public static final String EXTRA_STOPPED = "STOPPED";
private static final int NOTIFICATION = 1;
private final Binder binder = new Binder();
private SwapType bluetoothSwap;
private WifiSwap wifiSwap;
private static final int TIMEOUT = 15 * 60 * 1000; // 15 mins
@ -304,7 +461,13 @@ public class SwapService extends Service {
@Nullable
private Timer timer;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public SwapType getBluetoothSwap() {
return bluetoothSwap;
}
public WifiSwap getWifiSwap() {
return wifiSwap;
}
public class Binder extends android.os.Binder {
public SwapService getService() {
@ -312,107 +475,55 @@ public class SwapService extends Service {
}
}
@Override
public void onCreate() {
super.onCreate();
startForeground(NOTIFICATION, createNotification());
localBroadcastManager = LocalBroadcastManager.getInstance(this);
swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
LocalHTTPDManager.start(this);
Utils.debugLog(TAG, "Creating swap service.");
startForeground(NOTIFICATION, createNotification());
deleteAllSwapRepos();
CacheSwapAppsService.startCaching(this);
swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
SwapService.putBluetoothEnabledBeforeSwap(bluetoothAdapter.isEnabled());
if (bluetoothAdapter.isEnabled()) {
BluetoothManager.start(this);
}
registerReceiver(bluetoothScanModeChanged,
new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
}
wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class);
wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if (wifiManager != null) {
SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled());
}
appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, "")));
bluetoothSwap = BluetoothSwap.create(this);
wifiSwap = new WifiSwap(this, wifiManager);
Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener);
localBroadcastManager.registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST));
localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS));
localBroadcastManager.registerReceiver(bluetoothPeerFound, new IntentFilter(BluetoothManager.ACTION_FOUND));
localBroadcastManager.registerReceiver(bonjourPeerFound, new IntentFilter(BonjourManager.ACTION_FOUND));
localBroadcastManager.registerReceiver(bonjourPeerRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED));
localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS));
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
if (getHotspotActivatedUserPreference()) {
WifiApControl wifiApControl = WifiApControl.getInstance(this);
if (wifiApControl != null) {
wifiApControl.enable();
if (getBluetoothVisibleUserPreference()) {
Utils.debugLog(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically.");
bluetoothSwap.startInBackground(); // TODO replace with Intent to SwapService
} else {
Utils.debugLog(TAG, "Bluetooth was NOT enabled last time user swapped, starting not visible.");
}
} else if (getWifiVisibleUserPreference()) {
if (wifiManager != null) {
wifiManager.setWifiEnabled(true);
if (getWifiVisibleUserPreference()) {
Utils.debugLog(TAG, "Previously the user enabled WiFi swap, so enabling again automatically.");
wifiSwap.startInBackground(); // TODO replace with Intent to SwapService
} else {
Utils.debugLog(TAG, "WiFi was NOT enabled last time user swapped, starting not visible.");
}
}
BonjourManager.start(this);
BonjourManager.setVisible(this, getWifiVisibleUserPreference() || getHotspotActivatedUserPreference());
}
private void askServerToSwapWithUs(final Repo repo) {
compositeDisposable.add(
Completable.fromAction(() -> {
String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString();
HttpURLConnection conn = null;
try {
URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap"));
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
try (OutputStream outputStream = conn.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream)) {
writer.write("repo=" + swapBackUri);
writer.flush();
}
int responseCode = conn.getResponseCode();
Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " +
"POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode);
} finally {
if (conn != null) {
conn.disconnect();
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(e -> {
Intent intent = new Intent(Downloader.ACTION_INTERRUPTED);
intent.setData(Uri.parse(repo.address));
intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage());
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
})
.subscribe()
);
}
/**
* This is for setting things up for when the {@code SwapService} was
* started by the user clicking on the initial start button. The things
* that must be run always on start-up go in {@link #onCreate()}.
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
deleteAllSwapRepos();
Intent startUiIntent = new Intent(this, SwapWorkflowActivity.class);
startUiIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(startUiIntent);
return START_NOT_STICKY;
return START_STICKY;
}
@Override
@ -424,43 +535,25 @@ public class SwapService extends Service {
@Override
public void onDestroy() {
compositeDisposable.dispose();
Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners.");
Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener);
localBroadcastManager.unregisterReceiver(onWifiChange);
localBroadcastManager.unregisterReceiver(bluetoothStatus);
localBroadcastManager.unregisterReceiver(bluetoothPeerFound);
localBroadcastManager.unregisterReceiver(bonjourPeerFound);
localBroadcastManager.unregisterReceiver(bonjourPeerRemoved);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
if (bluetoothAdapter != null) {
unregisterReceiver(bluetoothScanModeChanged);
if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) {
bluetoothAdapter.disable();
}
BluetoothManager.stop(this);
BonjourManager.stop(this);
LocalHTTPDManager.stop(this);
if (wifiManager != null && !wasWifiEnabledBeforeSwap()) {
wifiManager.setWifiEnabled(false);
}
WifiApControl ap = WifiApControl.getInstance(this);
if (ap != null) {
if (wasHotspotEnabledBeforeSwap()) {
ap.enable();
} else {
ap.disable();
}
}
stopPollingConnectedSwapRepo();
//TODO getBluetoothSwap().stopInBackground();
getWifiSwap().stopInBackground();
if (timer != null) {
timer.cancel();
}
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
stopForeground(true);
deleteAllSwapRepos();
@ -471,7 +564,7 @@ public class SwapService extends Service {
Intent intent = new Intent(this, SwapWorkflowActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
return new NotificationCompat.Builder(this, NotificationHelper.CHANNEL_SWAPS)
return new NotificationCompat.Builder(this)
.setContentTitle(getText(R.string.local_repo_running))
.setContentText(getText(R.string.touch_to_configure_local_repo))
.setSmallIcon(R.drawable.ic_nearby)
@ -497,151 +590,40 @@ public class SwapService extends Service {
}
}
private void startPollingConnectedSwapRepo() {
stopPollingConnectedSwapRepo();
pollConnectedSwapRepoTimer = new Timer("pollConnectedSwapRepoTimer", true);
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
if (peer != null) {
connectTo(peer);
}
}
};
pollConnectedSwapRepoTimer.schedule(timerTask, 5000);
}
public void stopPollingConnectedSwapRepo() {
if (pollConnectedSwapRepoTimer != null) {
pollConnectedSwapRepoTimer.cancel();
pollConnectedSwapRepoTimer = null;
}
}
/**
* Sets or resets the idel timer for {@link #TIMEOUT}ms, once the timer
* expires, this service and all things that rely on it will be stopped.
*/
public void initTimer() {
private void initTimer() {
// TODO replace by Android scheduler
if (timer != null) {
Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset.");
timer.cancel();
}
Utils.debugLog(TAG, "Initializing swap timeout to " + TIMEOUT + "ms minutes");
timer = new Timer(TAG, true);
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
Utils.debugLog(TAG, "Disabling swap because " + TIMEOUT + "ms passed.");
String msg = getString(R.string.swap_toast_closing_nearby_after_timeout);
Utils.showToastFromService(SwapService.this, msg, android.widget.Toast.LENGTH_LONG);
stop(SwapService.this);
}
}, TIMEOUT);
}
private void restartWiFiServices() {
boolean hasIp = FDroidApp.ipAddressString != null;
if (hasIp) {
LocalHTTPDManager.restart(this);
BonjourManager.restart(this);
BonjourManager.setVisible(this, getWifiVisibleUserPreference() || getHotspotActivatedUserPreference());
} else {
BonjourManager.stop(this);
LocalHTTPDManager.stop(this);
}
}
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final Preferences.ChangeListener httpsEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
restartWiFiServices();
Log.i(TAG, "Swap over HTTPS preference changed.");
stopWifiIfEnabled(true);
}
};
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
restartWiFiServices();
boolean hasIp = FDroidApp.ipAddressString != null;
stopWifiIfEnabled(hasIp);
}
};
private final BroadcastReceiver bluetoothStatus = new SwapStateChangeReceiver();
private final BroadcastReceiver localRepoStatus = new SwapStateChangeReceiver();
/**
* When swapping is setup, then start the index polling.
*/
private class SwapStateChangeReceiver extends BroadcastReceiver {
private final BroadcastReceiver pollForUpdatesReceiver = new PollForUpdatesReceiver();
@Override
public void onReceive(Context context, Intent intent) {
int bluetoothStatus = intent.getIntExtra(BluetoothManager.ACTION_STATUS, -1);
int wifiStatus = intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1);
if (bluetoothStatus == BluetoothManager.STATUS_STARTED
|| wifiStatus == LocalRepoService.STATUS_STARTED) {
localBroadcastManager.registerReceiver(pollForUpdatesReceiver,
new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
} else {
localBroadcastManager.unregisterReceiver(pollForUpdatesReceiver);
}
}
}
/**
* Reschedule an index update if the last one was successful.
*/
private class PollForUpdatesReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1)) {
case UpdateService.STATUS_COMPLETE_AND_SAME:
case UpdateService.STATUS_COMPLETE_WITH_CHANGES:
startPollingConnectedSwapRepo();
break;
}
}
}
/**
* Handle events if the user or system changes the Bluetooth setup outside of F-Droid.
*/
private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
case BluetoothAdapter.SCAN_MODE_NONE:
BluetoothManager.stop(SwapService.this);
break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
BluetoothManager.start(SwapService.this);
break;
}
}
};
private final BroadcastReceiver bluetoothPeerFound = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
activePeers.add((Peer) intent.getParcelableExtra(BluetoothManager.EXTRA_PEER));
}
};
private final BroadcastReceiver bonjourPeerFound = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
activePeers.add((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
}
};
private final BroadcastReceiver bonjourPeerRemoved = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
activePeers.remove((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
}
};
}

View File

@ -17,25 +17,22 @@
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.localrepo;
import android.annotation.TargetApi;
import android.app.IntentService;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.support.v4.provider.DocumentFile;
import android.util.Log;
import android.widget.Toast;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.fdroid.fdroid.AddRepoIntentService;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
@ -44,13 +41,10 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import androidx.documentfile.provider.DocumentFile;
/**
* An {@link IntentService} subclass for handling asynchronous scanning of a
* removable storage device like an SD Card or USB OTG thumb drive using the
@ -63,11 +57,8 @@ import androidx.documentfile.provider.DocumentFile;
* {@link android.os.Build.VERSION_CODES#KITKAT android-19}, this approach is only
* workable if {@link android.content.Intent#ACTION_OPEN_DOCUMENT_TREE} is available.
* It was added in {@link android.os.Build.VERSION_CODES#LOLLIPOP android-21}.
* {@link android.os.storage.StorageVolume#createAccessIntent(String)} is also
* necessary to do this with any kind of rational UX.
*
* @see <a href="https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html">The Storage Situation: Removable Storage </a>
* @see <a href="https://commonsware.com/blog/2016/11/18/be-careful-scoped-directory-access.html">Be Careful with Scoped Directory Access</a>
* @see <a href="https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html"> The Storage Situation: Removable Storage </a>
* @see <a href="https://developer.android.com/training/articles/scoped-directory-access.html">Using Scoped Directory Access</a>
* @see <a href="https://developer.android.com/guide/topics/providers/document-provider.html">Open Files using Storage Access Framework</a>
*/
@ -75,44 +66,19 @@ import androidx.documentfile.provider.DocumentFile;
public class TreeUriScannerIntentService extends IntentService {
public static final String TAG = "TreeUriScannerIntentSer";
private static final String ACTION_SCAN_TREE_URI = "org.fdroid.fdroid.nearby.action.SCAN_TREE_URI";
/**
* @see <a href="https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_r38/core/java/android/provider/DocumentsContract.java#238">DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY</a>
* @see <a href="https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_r38/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java#70">ExternalStorageProvider.AUTHORITY</a>
*/
public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents";
private static final String ACTION_SCAN_TREE_URI = "org.fdroid.fdroid.localrepo.action.SCAN_TREE_URI";
public TreeUriScannerIntentService() {
super("TreeUriScannerIntentService");
}
public static void scan(Context context, Uri data) {
if (Preferences.get().isScanRemovableStorageEnabled()) {
Intent intent = new Intent(context, TreeUriScannerIntentService.class);
intent.setAction(ACTION_SCAN_TREE_URI);
intent.setData(data);
context.startService(intent);
}
/**
* Now determine if it is External Storage that must be handled by the
* {@link TreeUriScannerIntentService} or whether it is External Storage
* like an SD Card that can be directly accessed via the file system.
*/
public static void onActivityResult(Context context, Intent intent) {
if (intent == null) {
return;
}
Uri uri = intent.getData();
if (uri != null) {
if (Build.VERSION.SDK_INT >= 19) {
ContentResolver contentResolver = context.getContentResolver();
int perms = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
contentResolver.takePersistableUriPermission(uri, perms);
}
String msg = String.format(context.getString(R.string.swap_toast_using_path), uri.toString());
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
scan(context, uri);
}
}
@Override
@ -129,34 +95,20 @@ public class TreeUriScannerIntentService extends IntentService {
searchDirectory(treeFile);
}
/**
* Recursively search for {@link IndexV1Updater#SIGNED_FILE_NAME} starting
* from the given directory, looking at files first before recursing into
* directories. This is "depth last" since the index file is much more
* likely to be shallow than deep, and there can be a lot of files to
* search through starting at 4 or more levels deep, like the fdroid
* icons dirs and the per-app "external storage" dirs.
*/
private void searchDirectory(DocumentFile documentFileDir) {
DocumentFile[] documentFiles = documentFileDir.listFiles();
if (documentFiles == null) {
return;
}
boolean foundIndex = false;
ArrayList<DocumentFile> dirs = new ArrayList<>();
for (DocumentFile documentFile : documentFiles) {
if (documentFile.isDirectory()) {
dirs.add(documentFile);
} else if (!foundIndex) {
searchDirectory(documentFile);
} else {
if (IndexV1Updater.SIGNED_FILE_NAME.equals(documentFile.getName())) {
registerRepo(documentFile);
foundIndex = true;
}
}
}
for (DocumentFile dir : dirs) {
searchDirectory(dir);
}
}
/**
@ -171,7 +123,9 @@ public class TreeUriScannerIntentService extends IntentService {
private void registerRepo(DocumentFile index) {
InputStream inputStream = null;
try {
Log.i(TAG, "FOUND: " + index.getUri());
inputStream = getContentResolver().openInputStream(index.getUri());
Log.i(TAG, "repo URL: " + index.getParentFile().getUri());
registerRepo(this, inputStream, index.getParentFile().getUri());
} catch (IOException | IndexUpdater.SigningException e) {
e.printStackTrace();
@ -191,6 +145,7 @@ public class TreeUriScannerIntentService extends IntentService {
JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexV1Updater.DATA_FILE_NAME);
IOUtils.readLines(jarFile.getInputStream(indexEntry));
Certificate certificate = IndexUpdater.getSigningCertFromJar(indexEntry);
Log.i(TAG, "Got certificate: " + certificate);
String fingerprint = Utils.calcFingerprint(certificate);
Log.i(TAG, "Got fingerprint: " + fingerprint);
destFile.delete();

View File

@ -0,0 +1,126 @@
package org.fdroid.fdroid.localrepo.peers;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;
@SuppressWarnings("LineLength")
final class BluetoothFinder extends PeerFinder {
public static Observable<Peer> createBluetoothObservable(final Context context) {
return Observable.create(new Observable.OnSubscribe<Peer>() {
@Override
public void call(Subscriber<? super Peer> subscriber) {
final BluetoothFinder finder = new BluetoothFinder(context, subscriber);
subscriber.add(Subscriptions.create(new Action0() {
@Override
public void call() {
finder.cancel();
}
}));
finder.scan();
}
});
}
private static final String TAG = "BluetoothFinder";
private final BluetoothAdapter adapter;
private BluetoothFinder(Context context, Subscriber<? super Peer> subscriber) {
super(context, subscriber);
adapter = BluetoothAdapter.getDefaultAdapter();
}
private BroadcastReceiver deviceFoundReceiver;
private BroadcastReceiver scanCompleteReceiver;
private void scan() {
if (adapter == null) {
Log.i(TAG, "Not scanning for bluetooth peers to swap with, couldn't find a bluetooth adapter on this device.");
return;
}
isScanning = true;
if (deviceFoundReceiver == null) {
deviceFoundReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
onDeviceFound(device);
}
}
};
context.registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
}
if (scanCompleteReceiver == null) {
scanCompleteReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (isScanning) {
Utils.debugLog(TAG, "Scan complete, but we haven't been asked to stop scanning yet, so will restart scan.");
startDiscovery();
}
}
};
// TODO: Unregister this receiver at the appropriate time.
context.registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
}
startDiscovery();
}
private void startDiscovery() {
if (adapter.isDiscovering()) {
// TODO: Can we reset the discovering timeout, so that it doesn't, e.g. time out in 3
// seconds because we had already almost completed the previous scan? We could
// cancelDiscovery(), but then it will probably prompt the user again.
Utils.debugLog(TAG, "Requested bluetooth scan when already scanning, so will ignore request.");
return;
}
if (!adapter.startDiscovery()) {
Log.e(TAG, "Couldn't start bluetooth scanning.");
}
}
private void cancel() {
if (adapter != null) {
Utils.debugLog(TAG, "Stopping bluetooth discovery.");
adapter.cancelDiscovery();
}
isScanning = false;
}
private void onDeviceFound(BluetoothDevice device) {
if (device != null && device.getName() != null &&
(device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA ||
device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA ||
device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.PHONE_SMART)) {
subscriber.onNext(new BluetoothPeer(device));
}
}
}

View File

@ -1,36 +1,16 @@
package org.fdroid.fdroid.nearby.peers;
package org.fdroid.fdroid.localrepo.peers;
import android.bluetooth.BluetoothClass.Device;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.R;
import androidx.annotation.Nullable;
import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
public class BluetoothPeer implements Peer {
private static final String BLUETOOTH_NAME_TAG = "FDroid:";
private final BluetoothDevice device;
/**
* Return a instance if the {@link BluetoothDevice} is a device that could
* host a swap repo.
*/
@Nullable
public static BluetoothPeer getInstance(@Nullable BluetoothDevice device) {
if (device != null && device.getName() != null &&
(device.getBluetoothClass().getDeviceClass() == Device.COMPUTER_HANDHELD_PC_PDA
|| device.getBluetoothClass().getDeviceClass() == Device.COMPUTER_PALM_SIZE_PC_PDA
|| device.getBluetoothClass().getDeviceClass() == Device.PHONE_SMART)) {
return new BluetoothPeer(device);
}
return null;
}
private BluetoothPeer(BluetoothDevice device) {
public BluetoothPeer(BluetoothDevice device) {
this.device = device;
}
@ -41,18 +21,19 @@ public class BluetoothPeer implements Peer {
@Override
public String getName() {
return device.getName().replaceAll("^" + BLUETOOTH_NAME_TAG, "");
return device.getName().replaceAll("^" + BluetoothSwap.BLUETOOTH_NAME_TAG, "");
}
@Override
public int getIcon() {
return R.drawable.ic_bluetooth;
return R.drawable.ic_bluetooth_white;
}
@Override
public boolean equals(Object peer) {
return peer instanceof BluetoothPeer
&& TextUtils.equals(((BluetoothPeer) peer).device.getAddress(), device.getAddress());
return peer != null
&& peer instanceof BluetoothPeer
&& ((BluetoothPeer) peer).device.getAddress().equals(device.getAddress());
}
@Override
@ -67,7 +48,7 @@ public class BluetoothPeer implements Peer {
/**
* Return the fingerprint of the signing key, or {@code null} if it is not set.
* <p>
*
* This is not yet stored for Bluetooth connections. Once a device is connected to a bluetooth
* socket, if we trust it enough to accept a fingerprint from it somehow, then we may as well
* trust it enough to receive an index from it that contains a fingerprint we can use.

View File

@ -0,0 +1,162 @@
package org.fdroid.fdroid.localrepo.peers;
import android.content.Context;
import android.net.wifi.WifiManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import java.io.IOException;
import java.net.InetAddress;
@SuppressWarnings("LineLength")
final class BonjourFinder extends PeerFinder implements ServiceListener {
public static Observable<Peer> createBonjourObservable(final Context context) {
return Observable.create(new Observable.OnSubscribe<Peer>() {
@Override
public void call(Subscriber<? super Peer> subscriber) {
final BonjourFinder finder = new BonjourFinder(context, subscriber);
subscriber.add(Subscriptions.create(new Action0() {
@Override
public void call() {
finder.cancel();
}
}));
finder.scan();
}
});
}
private static final String TAG = "BonjourFinder";
private static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
private static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
private JmDNS jmdns;
private WifiManager wifiManager;
private WifiManager.MulticastLock multicastLock;
private BonjourFinder(Context context, Subscriber<? super Peer> subscriber) {
super(context, subscriber);
}
private void scan() {
Utils.debugLog(TAG, "Requested Bonjour (mDNS) scan for peers.");
if (wifiManager == null) {
wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiManager.createMulticastLock(context.getPackageName());
multicastLock.setReferenceCounted(false);
}
if (isScanning) {
Utils.debugLog(TAG, "Requested Bonjour scan, but already scanning. But we will still try to explicitly scan for services.");
return;
}
isScanning = true;
multicastLock.acquire();
try {
Utils.debugLog(TAG, "Searching for Bonjour (mDNS) clients...");
jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
} catch (IOException e) {
subscriber.onError(e);
return;
}
Utils.debugLog(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE);
jmdns.addServiceListener(HTTP_SERVICE_TYPE, this);
jmdns.addServiceListener(HTTPS_SERVICE_TYPE, this);
listServices();
}
private void listServices() {
Utils.debugLog(TAG, "Explicitly querying for services, in addition to waiting for notifications.");
addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE));
addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE));
}
@Override
public void serviceRemoved(ServiceEvent event) {
}
@Override
public void serviceAdded(final ServiceEvent event) {
// TODO: Get clarification, but it looks like this is:
// 1) Identifying that there is _a_ bonjour service available
// 2) Adding it to the list to give some sort of feedback to the user
// 3) Requesting more detailed info in an async manner
// 4) If that is in fact an fdroid repo (after requesting info), then add it again
// so that more detailed info can be shown to the user.
//
// If so, when is the old one removed?
addFDroidService(event.getInfo());
Utils.debugLog(TAG, "Found JmDNS service, now requesting further details of service");
jmdns.requestServiceInfo(event.getType(), event.getName(), true);
}
@Override
public void serviceResolved(ServiceEvent event) {
addFDroidService(event.getInfo());
}
private void addFDroidServices(ServiceInfo[] services) {
for (ServiceInfo info : services) {
addFDroidService(info);
}
}
/**
* Broadcasts the fact that a Bonjour peer was found to swap with.
* Checks that the service is an F-Droid service, and also that it is not the F-Droid service
* for this device (by comparing its signing fingerprint to our signing fingerprint).
*/
private void addFDroidService(ServiceInfo serviceInfo) {
final String type = serviceInfo.getPropertyString("type");
final String fingerprint = serviceInfo.getPropertyString("fingerprint");
final boolean isFDroid = type != null && type.startsWith("fdroidrepo");
final boolean isSelf = FDroidApp.repo != null && fingerprint != null && fingerprint.equalsIgnoreCase(FDroidApp.repo.fingerprint);
if (isFDroid && !isSelf) {
Utils.debugLog(TAG, "Found F-Droid swap Bonjour service:\n" + serviceInfo);
subscriber.onNext(new BonjourPeer(serviceInfo));
} else {
if (isSelf) {
Utils.debugLog(TAG, "Ignoring Bonjour service because it belongs to this device:\n" + serviceInfo);
} else {
Utils.debugLog(TAG, "Ignoring Bonjour service because it doesn't look like an F-Droid swap repo:\n" + serviceInfo);
}
}
}
private void cancel() {
Utils.debugLog(TAG, "Cancelling BonjourFinder, releasing multicast lock, removing jmdns service listeners");
if (multicastLock != null) {
multicastLock.release();
}
isScanning = false;
if (jmdns == null) {
return;
}
jmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
jmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
jmdns = null;
}
}

View File

@ -1,42 +1,16 @@
package org.fdroid.fdroid.nearby.peers;
package org.fdroid.fdroid.localrepo.peers;
import android.net.Uri;
import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.FDroidServiceInfo;
import androidx.annotation.Nullable;
public class BonjourPeer extends WifiPeer {
private static final String TAG = "BonjourPeer";
public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name";
public static final String PATH = "path";
public static final String TYPE = "type";
private final FDroidServiceInfo serviceInfo;
/**
* Return a instance if the {@link ServiceInfo} is fully resolved and does
* not represent this device, but something else on the network.
*/
@Nullable
public static BonjourPeer getInstance(ServiceInfo serviceInfo) {
String type = serviceInfo.getPropertyString(TYPE);
String fingerprint = serviceInfo.getPropertyString(FINGERPRINT);
if (type == null || !type.startsWith("fdroidrepo")
|| TextUtils.equals(FDroidApp.repo.fingerprint, fingerprint)) {
return null;
}
return new BonjourPeer(serviceInfo);
}
private BonjourPeer(ServiceInfo serviceInfo) {
public BonjourPeer(ServiceInfo serviceInfo) {
this.serviceInfo = new FDroidServiceInfo(serviceInfo);
this.name = serviceInfo.getDomain();
this.uri = Uri.parse(this.serviceInfo.getRepoAddress());
@ -53,6 +27,15 @@ public class BonjourPeer extends WifiPeer {
return serviceInfo.getName();
}
@Override
public boolean equals(Object peer) {
if (peer != null && peer instanceof BonjourPeer) {
BonjourPeer that = (BonjourPeer) peer;
return this.getFingerprint().equals(that.getFingerprint());
}
return false;
}
@Override
public int hashCode() {
String fingerprint = getFingerprint();

View File

@ -0,0 +1,19 @@
package org.fdroid.fdroid.localrepo.peers;
import android.os.Parcelable;
import android.support.annotation.DrawableRes;
public interface Peer extends Parcelable {
String getName();
@DrawableRes int getIcon();
boolean equals(Object peer);
String getRepoAddress();
String getFingerprint();
boolean shouldPromptForSwapBack();
}

View File

@ -0,0 +1,31 @@
package org.fdroid.fdroid.localrepo.peers;
import android.content.Context;
import rx.Observable;
import rx.Subscriber;
import rx.schedulers.Schedulers;
/**
* Searches for other devices in the vicinity, using specific technologies.
* Once found, emits a {@link Peer} to interested {@link Subscriber}s.
*/
public abstract class PeerFinder {
protected boolean isScanning;
protected final Context context;
protected final Subscriber<? super Peer> subscriber;
protected PeerFinder(Context context, Subscriber<? super Peer> subscriber) {
this.context = context;
this.subscriber = subscriber;
}
public static Observable<Peer> createObservable(final Context context) {
return Observable.merge(
BluetoothFinder.createBluetoothObservable(context).subscribeOn(Schedulers.newThread()),
BonjourFinder.createBonjourObservable(context).subscribeOn(Schedulers.newThread())
);
}
}

View File

@ -1,8 +1,7 @@
package org.fdroid.fdroid.nearby.peers;
package org.fdroid.fdroid.localrepo.peers;
import android.net.Uri;
import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.NewRepoConfig;
@ -27,35 +26,6 @@ public class WifiPeer implements Peer {
this.shouldPromptForSwapBack = shouldPromptForSwapBack;
}
/**
* Return if this instance points to the same device as that instance, even
* if some of the configuration details are not the same, like whether one
* instance supplies the fingerprint and the other does not, then use IP
* address and port number.
*/
@Override
public boolean equals(Object peer) {
if (peer instanceof BluetoothPeer) {
return false;
}
String fingerprint = getFingerprint();
if (this instanceof BonjourPeer && peer instanceof BonjourPeer) {
BonjourPeer that = (BonjourPeer) peer;
return TextUtils.equals(this.getFingerprint(), that.getFingerprint());
} else {
WifiPeer that = (WifiPeer) peer;
if (!TextUtils.isEmpty(fingerprint) && TextUtils.equals(this.getFingerprint(), that.getFingerprint())) {
return true;
}
return TextUtils.equals(this.getRepoAddress(), that.getRepoAddress());
}
}
@Override
public int hashCode() {
return (uri.getHost() + uri.getPort()).hashCode();
}
@Override
public String getName() {
return name;
@ -63,7 +33,7 @@ public class WifiPeer implements Peer {
@Override
public int getIcon() {
return R.drawable.ic_wifi;
return R.drawable.ic_network_wifi_white;
}
@Override

View File

@ -0,0 +1,186 @@
package org.fdroid.fdroid.localrepo.type;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
@SuppressWarnings("LineLength")
public final class BluetoothSwap extends SwapType {
private static final String TAG = "BluetoothSwap";
public static final String BLUETOOTH_NAME_TAG = "FDroid:";
private static BluetoothSwap mInstance;
@NonNull
private final BluetoothAdapter adapter;
private boolean isDiscoverable;
@Nullable
private BluetoothServer server;
private String deviceBluetoothName;
public static SwapType create(@NonNull Context context) {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return new NoBluetoothType(context);
}
if (mInstance == null) {
mInstance = new BluetoothSwap(context, adapter);
}
return mInstance;
}
private BluetoothSwap(@NonNull Context context, @NonNull BluetoothAdapter adapter) {
super(context);
this.adapter = adapter;
}
@Override
public boolean isDiscoverable() {
return isDiscoverable;
}
@Override
public boolean isConnected() {
return server != null && server.isRunning() && super.isConnected();
}
@Override
public synchronized void start() {
if (isConnected()) {
Utils.debugLog(TAG, "already running, quitting start()");
return;
}
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
case BluetoothAdapter.SCAN_MODE_NONE:
setConnected(false);
break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
isDiscoverable = true;
if (server != null && server.isRunning()) {
setConnected(true);
}
break;
// Only other is BluetoothAdapter.SCAN_MODE_CONNECTABLE. For now don't handle that.
}
}
};
context.registerReceiver(receiver, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
/*
if (server != null) {
Utils.debugLog(TAG, "Attempting to start Bluetooth swap, but it appears to be running already. Will cancel it so it can be restarted.");
server.close();
server = null;
}*/
if (server == null) {
server = new BluetoothServer(this, context.getFilesDir());
}
sendBroadcast(SwapService.EXTRA_STARTING);
//store the original bluetoothname, and update this one to be unique
deviceBluetoothName = adapter.getName();
/*
Utils.debugLog(TAG, "Prefixing Bluetooth adapter name with " + BLUETOOTH_NAME_TAG + " to make it identifiable as a swap device.");
if (!deviceBluetoothName.startsWith(BLUETOOTH_NAME_TAG)) {
adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName);
}
if (!adapter.getName().startsWith(BLUETOOTH_NAME_TAG)) {
Log.e(TAG, "Couldn't change the name of the Bluetooth adapter, it will not get recognized by other swap clients.");
// TODO: Should we bail here?
}*/
if (!adapter.isEnabled()) {
Utils.debugLog(TAG, "Bluetooth adapter is disabled, attempting to enable.");
if (!adapter.enable()) {
Utils.debugLog(TAG, "Could not enable Bluetooth adapter, so bailing out of Bluetooth swap.");
setConnected(false);
return;
}
}
if (adapter.isEnabled()) {
setConnected(true);
} else {
Log.i(TAG, "Didn't start Bluetooth swapping server, because Bluetooth is disabled and couldn't be enabled.");
setConnected(false);
}
}
/**
* Don't try to start BT in the background. you can only start/stop a BT server once, else new connections don't work.
*/
@Override
public void stopInBackground() {
stop();
}
@Override
public void stop() {
if (server != null && server.isAlive()) {
server.close();
setConnected(false);
/*
if (receiver != null) {
context.unregisterReceiver(receiver);
receiver = null;
}
*/
} else {
Log.i(TAG, "Attempting to stop Bluetooth swap, but it is not currently running.");
}
}
protected void onStopped() {
Utils.debugLog(TAG, "Resetting bluetooth device name to " + deviceBluetoothName + " after swapping.");
adapter.setName(deviceBluetoothName);
}
@Override
public String getBroadcastAction() {
return SwapService.BLUETOOTH_STATE_CHANGE;
}
private static class NoBluetoothType extends SwapType {
NoBluetoothType(@NonNull Context context) {
super(context);
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
protected String getBroadcastAction() {
return null;
}
}
}

View File

@ -0,0 +1,112 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
/**
* Sends a {@link SwapService#BONJOUR_STATE_CHANGE} broadcasts when starting, started or stopped.
*/
public class BonjourBroadcast extends SwapType {
private static final String TAG = "BonjourBroadcast";
private JmDNS jmdns;
private ServiceInfo pairService;
public BonjourBroadcast(Context context) {
super(context);
}
@Override
public void start() {
Utils.debugLog(TAG, "Preparing to start Bonjour service.");
sendBroadcast(SwapService.EXTRA_STARTING);
InetAddress address = getDeviceAddress();
if (address == null) {
Log.e(TAG, "Starting Bonjour service, but couldn't ascertain IP address."
+ " Seems we are not connected to a network.");
return;
}
/*
* a ServiceInfo can only be registered with a single instance
* of JmDNS, and there is only ever a single LocalHTTPD port to
* advertise anyway.
*/
if (pairService != null || jmdns != null) {
clearCurrentMDNSService();
}
String repoName = Preferences.get().getLocalRepoName();
HashMap<String, String> values = new HashMap<>();
values.put("path", "/fdroid/repo");
values.put("name", repoName);
values.put("fingerprint", FDroidApp.repo.fingerprint);
String type;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
values.put("type", "fdroidrepos");
type = "_https._tcp.local.";
} else {
values.put("type", "fdroidrepo");
type = "_http._tcp.local.";
}
try {
Utils.debugLog(TAG, "Starting bonjour service...");
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
jmdns = JmDNS.create(address);
jmdns.registerService(pairService);
setConnected(true);
Utils.debugLog(TAG, "... Bounjour service started.");
} catch (IOException e) {
Log.e(TAG, "Error while registering jmdns service", e);
setConnected(false);
}
}
@Override
public void stop() {
Utils.debugLog(TAG, "Unregistering MDNS service...");
clearCurrentMDNSService();
setConnected(false);
}
private void clearCurrentMDNSService() {
if (jmdns != null) {
jmdns.unregisterAllServices();
Utils.closeQuietly(jmdns);
pairService = null;
jmdns = null;
}
}
@Override
public String getBroadcastAction() {
return SwapService.BONJOUR_STATE_CHANGE;
}
@Nullable
private InetAddress getDeviceAddress() {
if (FDroidApp.ipAddressString != null) {
try {
return InetAddress.getByName(FDroidApp.ipAddressString);
} catch (UnknownHostException ignored) {
}
}
return null;
}
}

View File

@ -0,0 +1,109 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
/**
* There is lots of common functionality, and a common API among different communication protocols
* associated with the swap process. This includes Bluetooth visability, Bonjour visability,
* and the web server which serves info for swapping. This class provides a common API for
* starting and stopping these services. In addition, it helps with the process of sending broadcast
* intents in response to the thing starting or stopping.
*/
public abstract class SwapType {
private static final String TAG = "SwapType";
private boolean isConnected;
@NonNull
protected final Context context;
public SwapType(@NonNull Context context) {
this.context = context;
}
public abstract void start();
public abstract void stop();
protected abstract String getBroadcastAction();
public boolean isDiscoverable() {
return isConnected();
}
protected final void setConnected(boolean connected) {
if (connected) {
isConnected = true;
sendBroadcast(SwapService.EXTRA_STARTED);
} else {
isConnected = false;
onStopped();
sendBroadcast(SwapService.EXTRA_STOPPED);
}
}
protected void onStopped() { }
/**
* Sends either a {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTING},
* {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTED} or
* {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STOPPED} broadcast.
*/
protected final void sendBroadcast(String extra) {
if (getBroadcastAction() != null) {
Intent intent = new Intent(getBroadcastAction());
intent.putExtra(extra, true);
Utils.debugLog(TAG, "Sending broadcast " + extra + " from " + getClass().getSimpleName());
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
public boolean isConnected() {
return isConnected;
}
public void startInBackground() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
start();
return null;
}
}.execute();
}
private void ensureRunning() {
if (!isConnected()) {
start();
}
}
public void ensureRunningInBackground() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ensureRunning();
return null;
}
}.execute();
}
public void stopInBackground() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
stop();
return null;
}
}.execute();
}
}

View File

@ -0,0 +1,181 @@
package org.fdroid.fdroid.localrepo.type;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
import rx.Single;
import rx.SingleSubscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.functions.Func2;
import rx.schedulers.Schedulers;
import java.io.IOException;
import java.net.BindException;
import java.util.Random;
@SuppressWarnings("LineLength")
public class WifiSwap extends SwapType {
private static final String TAG = "WifiSwap";
private Handler webServerThreadHandler;
private LocalHTTPD localHttpd;
private final BonjourBroadcast bonjourBroadcast;
private final WifiManager wifiManager;
public WifiSwap(Context context, WifiManager wifiManager) {
super(context);
bonjourBroadcast = new BonjourBroadcast(context);
this.wifiManager = wifiManager;
}
protected String getBroadcastAction() {
return SwapService.WIFI_STATE_CHANGE;
}
public BonjourBroadcast getBonjour() {
return bonjourBroadcast;
}
@Override
public void start() {
wifiManager.setWifiEnabled(true);
Utils.debugLog(TAG, "Preparing swap webserver.");
sendBroadcast(SwapService.EXTRA_STARTING);
if (FDroidApp.ipAddressString == null) {
Log.e(TAG, "Not starting swap webserver, because we don't seem to be connected to a network.");
setConnected(false);
}
Single.zip(
Single.create(getWebServerTask()),
Single.create(getBonjourTask()),
new Func2<Boolean, Boolean, Boolean>() {
@Override
public Boolean call(Boolean webServerTask, Boolean bonjourServiceTask) {
return bonjourServiceTask && webServerTask;
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.subscribe(new Action1<Boolean>() {
@Override
public void call(Boolean success) {
setConnected(success);
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
setConnected(false);
}
});
}
/**
* A task which starts the {@link WifiSwap#bonjourBroadcast} and then emits a `true` value at
* the end.
*/
private Single.OnSubscribe<Boolean> getBonjourTask() {
return new Single.OnSubscribe<Boolean>() {
@Override
public void call(SingleSubscriber<? super Boolean> singleSubscriber) {
bonjourBroadcast.start();
// TODO: Be more intelligent about failures here so that we can invoke
// singleSubscriber.onError() in the appropriate circumstances.
singleSubscriber.onSuccess(true);
}
};
}
/**
* Constructs a new {@link Thread} for the webserver to run on. If successful, it will also
* populate the webServerThreadHandler property and bind it to that particular thread. This
* allows messages to be sent to the webserver thread by posting messages to that handler.
*/
private Single.OnSubscribe<Boolean> getWebServerTask() {
return new Single.OnSubscribe<Boolean>() {
@Override
public void call(final SingleSubscriber<? super Boolean> singleSubscriber) {
new Thread(new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@Override
public void run() {
localHttpd = new LocalHTTPD(
context,
FDroidApp.ipAddressString,
FDroidApp.port,
context.getFilesDir(),
Preferences.get().isLocalRepoHttpsEnabled());
Looper.prepare(); // must be run before creating a Handler
webServerThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
localHttpd.stop();
Looper looper = Looper.myLooper();
if (looper == null) {
Log.e(TAG, "Looper.myLooper() was null for sum reason while shutting down the swap webserver.");
} else {
looper.quit();
}
}
};
try {
Utils.debugLog(TAG, "Starting swap webserver...");
localHttpd.start();
Utils.debugLog(TAG, "Swap webserver started.");
singleSubscriber.onSuccess(true);
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
WifiStateChangeService.start(context, null);
singleSubscriber.onError(new Exception("port " + prev + " occupied, trying on " + FDroidApp.port + "!"));
} catch (IOException e) {
Log.e(TAG, "Could not start local repo HTTP server", e);
singleSubscriber.onError(e);
}
Looper.loop(); // start the message receiving loop
}
}).start();
}
};
}
@Override
public void stop() {
sendBroadcast(SwapService.EXTRA_STOPPING);
if (webServerThreadHandler == null) {
Log.i(TAG, "null handler in stopWebServer");
} else {
Utils.debugLog(TAG, "Sending message to swap webserver to stop it.");
Message msg = webServerThreadHandler.obtainMessage();
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
webServerThreadHandler.sendMessage(msg);
}
// Stop the Bonjour stuff after asking the webserver to stop. This is not required in this
// order, but it helps. In practice, the Bonjour stuff takes a second or two to stop. This
// should give enough time for the message we posted above to reach the web server thread
// and for the webserver to thus be stopped.
bonjourBroadcast.stop();
setConnected(false);
}
}

View File

@ -1,32 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import java.io.IOException;
public class BluetoothClient {
private static final String TAG = "BluetoothClient";
private final BluetoothDevice device;
public BluetoothClient(String macAddress) {
device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
}
public BluetoothConnection openConnection() throws IOException {
BluetoothConnection connection = null;
try {
BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
connection = new BluetoothConnection(socket);
connection.open();
return connection;
} finally {
if (connection != null) {
connection.closeQuietly();
}
}
}
}

View File

@ -1,179 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.peers.BluetoothPeer;
import java.lang.ref.WeakReference;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Manage the {@link android.bluetooth.BluetoothAdapter}in a {@link HandlerThread}.
* The start process is in {@link HandlerThread#onLooperPrepared()} so that it is
* always started before any messages get delivered from the queue.
*
* @see BonjourManager
* @see LocalRepoManager
*/
public class BluetoothManager {
private static final String TAG = "BluetoothManager";
public static final String ACTION_FOUND = "BluetoothNewPeer";
public static final String EXTRA_PEER = "extraBluetoothPeer";
public static final String ACTION_STATUS = "BluetoothStatus";
public static final String EXTRA_STATUS = "BluetoothStatusExtra";
public static final int STATUS_STARTING = 0;
public static final int STATUS_STARTED = 1;
public static final int STATUS_STOPPING = 2;
public static final int STATUS_STOPPED = 3;
public static final int STATUS_ERROR = 0xffff;
private static final int STOP = 5709;
private static WeakReference<Context> context;
private static Handler handler;
private static volatile HandlerThread handlerThread;
private static BluetoothAdapter bluetoothAdapter;
/**
* Stops the Bluetooth adapter, triggering a status broadcast via {@link #ACTION_STATUS}.
* {@link #STATUS_STOPPED} can be broadcast multiple times for the same session,
* so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
*/
public static void stop(Context context) {
BluetoothManager.context = new WeakReference<>(context);
if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
Log.w(TAG, "handlerThread is already stopped, doing nothing!");
sendBroadcast(STATUS_STOPPED, null);
return;
}
sendBroadcast(STATUS_STOPPING, null);
handler.sendEmptyMessage(STOP);
}
/**
* Starts the service, triggering a status broadcast via {@link #ACTION_STATUS}.
* {@link #STATUS_STARTED} can be broadcast multiple times for the same session,
* so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
*/
public static void start(final Context context) {
BluetoothManager.context = new WeakReference<>(context);
if (handlerThread != null && handlerThread.isAlive()) {
sendBroadcast(STATUS_STARTED, null);
return;
}
sendBroadcast(STATUS_STARTING, null);
final BluetoothServer bluetoothServer = new BluetoothServer(context.getFilesDir());
handlerThread = new HandlerThread("BluetoothManager", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
@Override
protected void onLooperPrepared() {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
localBroadcastManager.registerReceiver(bluetoothDeviceFound,
new IntentFilter(BluetoothDevice.ACTION_FOUND));
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
String name = bluetoothAdapter.getName();
if (name != null) {
SwapService.putBluetoothNameBeforeSwap(name);
}
if (!bluetoothAdapter.enable()) {
sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth));
return;
}
bluetoothServer.start();
if (bluetoothAdapter.startDiscovery()) {
sendBroadcast(STATUS_STARTED, null);
} else {
sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth));
}
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
sendFoundBroadcast(context, device);
}
}
};
handlerThread.start();
handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
localBroadcastManager.unregisterReceiver(bluetoothDeviceFound);
bluetoothServer.close();
if (bluetoothAdapter != null) {
bluetoothAdapter.cancelDiscovery();
if (!SwapService.wasBluetoothEnabledBeforeSwap()) {
bluetoothAdapter.disable();
}
String name = SwapService.getBluetoothNameBeforeSwap();
if (name != null) {
bluetoothAdapter.setName(name);
}
}
handlerThread.quit();
handlerThread = null;
sendBroadcast(STATUS_STOPPED, null);
}
};
}
public static void restart(Context context) {
stop(context);
try {
handlerThread.join(10000);
} catch (InterruptedException | NullPointerException e) {
// ignored
}
start(context);
}
public static void setName(Context context, String name) {
// TODO
}
public static boolean isAlive() {
return handlerThread != null && handlerThread.isAlive();
}
private static void sendBroadcast(int status, String message) {
Intent intent = new Intent(ACTION_STATUS);
intent.putExtra(EXTRA_STATUS, status);
if (!TextUtils.isEmpty(message)) {
intent.putExtra(Intent.EXTRA_TEXT, message);
}
LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
}
private static final BroadcastReceiver bluetoothDeviceFound = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
sendFoundBroadcast(context, (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
}
};
private static void sendFoundBroadcast(Context context, BluetoothDevice device) {
BluetoothPeer bluetoothPeer = BluetoothPeer.getInstance(device);
if (bluetoothPeer == null) {
Utils.debugLog(TAG, "IGNORING: " + device);
return;
}
Intent intent = new Intent(ACTION_FOUND);
intent.putExtra(EXTRA_PEER, bluetoothPeer);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}

View File

@ -1,287 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.peers.BonjourPeer;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.util.HashMap;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Manage {@link JmDNS} in a {@link HandlerThread}. The start process is in
* {@link HandlerThread#onLooperPrepared()} so that it is always started before
* any messages get delivered from the queue.
*/
public class BonjourManager {
private static final String TAG = "BonjourManager";
public static final String ACTION_FOUND = "BonjourNewPeer";
public static final String ACTION_REMOVED = "BonjourPeerRemoved";
public static final String EXTRA_BONJOUR_PEER = "extraBonjourPeer";
public static final String ACTION_STATUS = "BonjourStatus";
public static final String EXTRA_STATUS = "BonjourStatusExtra";
public static final int STATUS_STARTING = 0;
public static final int STATUS_STARTED = 1;
public static final int STATUS_STOPPING = 2;
public static final int STATUS_STOPPED = 3;
public static final int STATUS_VISIBLE = 4;
public static final int STATUS_NOT_VISIBLE = 5;
public static final int STATUS_ERROR = 0xffff;
public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
private static final int STOP = 5709;
private static final int VISIBLE = 4151873;
private static final int NOT_VISIBLE = 144151873;
private static WeakReference<Context> context;
private static Handler handler;
private static volatile HandlerThread handlerThread;
private static ServiceInfo pairService;
private static JmDNS jmdns;
private static WifiManager.MulticastLock multicastLock;
public static boolean isAlive() {
return handlerThread != null && handlerThread.isAlive();
}
/**
* Stops the Bonjour/mDNS, triggering a status broadcast via {@link #ACTION_STATUS}.
* {@link #STATUS_STOPPED} can be broadcast multiple times for the same session,
* so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
*/
public static void stop(Context context) {
BonjourManager.context = new WeakReference<>(context);
if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
sendBroadcast(STATUS_STOPPED, null);
return;
}
sendBroadcast(STATUS_STOPPING, null);
handler.sendEmptyMessage(STOP);
}
public static void setVisible(Context context, boolean visible) {
BonjourManager.context = new WeakReference<>(context);
if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
Log.e(TAG, "handlerThread is stopped, not changing visibility!");
return;
}
if (visible) {
handler.sendEmptyMessage(VISIBLE);
} else {
handler.sendEmptyMessage(NOT_VISIBLE);
}
}
/**
* Starts the service, triggering a status broadcast via {@link #ACTION_STATUS}.
* {@link #STATUS_STARTED} can be broadcast multiple times for the same session,
* so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
*/
public static void start(Context context) {
start(context,
Preferences.get().getLocalRepoName(),
Preferences.get().isLocalRepoHttpsEnabled(),
httpServiceListener, httpsServiceListener);
}
/**
* Testable version, not for regular use.
*
* @see #start(Context)
*/
static void start(final Context context,
final String localRepoName, final boolean useHttps,
final ServiceListener httpServiceListener, final ServiceListener httpsServiceListener) {
BonjourManager.context = new WeakReference<>(context);
if (handlerThread != null && handlerThread.isAlive()) {
sendBroadcast(STATUS_STARTED, null);
return;
}
sendBroadcast(STATUS_STARTING, null);
final WifiManager wifiManager = ContextCompat.getSystemService(context, WifiManager.class);
handlerThread = new HandlerThread("BonjourManager", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
@Override
protected void onLooperPrepared() {
try {
InetAddress address = InetAddress.getByName(FDroidApp.ipAddressString);
jmdns = JmDNS.create(address);
jmdns.addServiceListener(HTTP_SERVICE_TYPE, httpServiceListener);
jmdns.addServiceListener(HTTPS_SERVICE_TYPE, httpsServiceListener);
multicastLock = wifiManager.createMulticastLock(context.getPackageName());
multicastLock.setReferenceCounted(false);
multicastLock.acquire();
sendBroadcast(STATUS_STARTED, null);
} catch (IOException e) {
if (handler != null) {
handler.removeMessages(VISIBLE);
handler.sendMessageAtFrontOfQueue(handler.obtainMessage(STOP));
}
Log.e(TAG, "Error while registering jmdns service", e);
sendBroadcast(STATUS_ERROR, e.getLocalizedMessage());
}
}
};
handlerThread.start();
handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case VISIBLE:
handleVisible(localRepoName, useHttps);
break;
case NOT_VISIBLE:
handleNotVisible();
break;
case STOP:
handleStop();
break;
}
}
private void handleVisible(String localRepoName, boolean useHttps) {
HashMap<String, String> values = new HashMap<>();
values.put(BonjourPeer.PATH, "/fdroid/repo");
values.put(BonjourPeer.NAME, localRepoName);
values.put(BonjourPeer.FINGERPRINT, FDroidApp.repo.fingerprint);
String type;
if (useHttps) {
values.put(BonjourPeer.TYPE, "fdroidrepos");
type = HTTPS_SERVICE_TYPE;
} else {
values.put(BonjourPeer.TYPE, "fdroidrepo");
type = HTTP_SERVICE_TYPE;
}
ServiceInfo newPairService = ServiceInfo.create(type, localRepoName, FDroidApp.port, 0, 0, values);
if (!newPairService.equals(pairService)) try {
if (pairService != null) {
jmdns.unregisterService(pairService);
}
jmdns.registerService(newPairService);
pairService = newPairService;
} catch (IOException e) {
e.printStackTrace();
sendBroadcast(STATUS_ERROR, e.getLocalizedMessage());
return;
}
sendBroadcast(STATUS_VISIBLE, null);
}
private void handleNotVisible() {
if (pairService != null) {
jmdns.unregisterService(pairService);
pairService = null;
}
sendBroadcast(STATUS_NOT_VISIBLE, null);
}
private void handleStop() {
if (multicastLock != null) {
multicastLock.release();
}
if (jmdns != null) {
jmdns.unregisterAllServices();
Utils.closeQuietly(jmdns);
pairService = null;
jmdns = null;
}
handlerThread.quit();
handlerThread = null;
sendBroadcast(STATUS_STOPPED, null);
}
};
}
public static void restart(Context context) {
restart(context,
Preferences.get().getLocalRepoName(),
Preferences.get().isLocalRepoHttpsEnabled(),
httpServiceListener, httpsServiceListener);
}
/**
* Testable version, not for regular use.
*
* @see #restart(Context)
*/
static void restart(final Context context,
final String localRepoName, final boolean useHttps,
final ServiceListener httpServiceListener, final ServiceListener httpsServiceListener) {
stop(context);
try {
handlerThread.join(10000);
} catch (InterruptedException | NullPointerException e) {
// ignored
}
start(context, localRepoName, useHttps, httpServiceListener, httpsServiceListener);
}
private static void sendBroadcast(String action, ServiceInfo serviceInfo) {
BonjourPeer bonjourPeer = BonjourPeer.getInstance(serviceInfo);
if (bonjourPeer == null) {
Utils.debugLog(TAG, "IGNORING: " + serviceInfo);
return;
}
Intent intent = new Intent(action);
intent.putExtra(EXTRA_BONJOUR_PEER, bonjourPeer);
LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
}
private static void sendBroadcast(int status, String message) {
Intent intent = new Intent(ACTION_STATUS);
intent.putExtra(EXTRA_STATUS, status);
if (!TextUtils.isEmpty(message)) {
intent.putExtra(Intent.EXTRA_TEXT, message);
}
LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
}
private static final ServiceListener httpServiceListener = new SwapServiceListener();
private static final ServiceListener httpsServiceListener = new SwapServiceListener();
private static class SwapServiceListener implements ServiceListener {
@Override
public void serviceAdded(ServiceEvent serviceEvent) {
// ignored, we only need resolved info
}
@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
sendBroadcast(ACTION_REMOVED, serviceEvent.getInfo());
}
@Override
public void serviceResolved(ServiceEvent serviceEvent) {
sendBroadcast(ACTION_FOUND, serviceEvent.getInfo());
}
}
}

View File

@ -1,129 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import java.io.IOException;
import java.net.BindException;
import java.util.Random;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Manage {@link LocalHTTPD} in a {@link HandlerThread};
*/
public class LocalHTTPDManager {
private static final String TAG = "LocalHTTPDManager";
public static final String ACTION_STARTED = "LocalHTTPDStarted";
public static final String ACTION_STOPPED = "LocalHTTPDStopped";
public static final String ACTION_ERROR = "LocalHTTPDError";
private static final int STOP = 5709;
private static Handler handler;
private static volatile HandlerThread handlerThread;
private static LocalHTTPD localHttpd;
public static void start(Context context) {
start(context, Preferences.get().isLocalRepoHttpsEnabled());
}
/**
* Testable version, not for regular use.
*
* @see #start(Context)
*/
static void start(final Context context, final boolean useHttps) {
if (handlerThread != null && handlerThread.isAlive()) {
Log.w(TAG, "handlerThread is already running, doing nothing!");
return;
}
handlerThread = new HandlerThread("LocalHTTPD", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
@Override
protected void onLooperPrepared() {
localHttpd = new LocalHTTPD(
context,
FDroidApp.ipAddressString,
FDroidApp.port,
context.getFilesDir(),
useHttps);
try {
localHttpd.start();
Intent intent = new Intent(ACTION_STARTED);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
WifiStateChangeService.start(context, null);
Intent intent = new Intent(ACTION_ERROR);
intent.putExtra(Intent.EXTRA_TEXT,
"port " + prev + " occupied, trying on " + FDroidApp.port + ": ("
+ e.getLocalizedMessage() + ")");
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} catch (IOException e) {
e.printStackTrace();
Intent intent = new Intent(ACTION_ERROR);
intent.putExtra(Intent.EXTRA_TEXT, e.getLocalizedMessage());
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
};
handlerThread.start();
handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
localHttpd.stop();
handlerThread.quit();
handlerThread = null;
}
};
}
public static void stop(Context context) {
if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
Log.w(TAG, "handlerThread is already stopped, doing nothing!");
handlerThread = null;
return;
}
handler.sendEmptyMessage(STOP);
Intent stoppedIntent = new Intent(ACTION_STOPPED);
LocalBroadcastManager.getInstance(context).sendBroadcast(stoppedIntent);
}
/**
* Run {@link #stop(Context)}, wait for it to actually stop, then run
* {@link #start(Context)}.
*/
public static void restart(Context context) {
restart(context, Preferences.get().isLocalRepoHttpsEnabled());
}
/**
* Testable version, not for regular use.
*
* @see #restart(Context)
*/
static void restart(Context context, boolean useHttps) {
stop(context);
try {
handlerThread.join(10000);
} catch (InterruptedException | NullPointerException e) {
// ignored
}
start(context, useHttps);
}
public static boolean isAlive() {
return handlerThread != null && handlerThread.isAlive();
}
}

View File

@ -1,148 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Process;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Handles setting up and generating the local repo used to swap apps, including
* the {@code index.jar}, the symlinks to the shared APKs, etc.
* <p/>
* The work is done in a {@link Thread} so that new incoming {@code Intents}
* are not blocked by processing. A new {@code Intent} immediately nullifies
* the current state because it means the user has chosen a different set of
* apps. That is also enforced here since new {@code Intent}s with the same
* {@link Set} of apps as the current one are ignored. Having the
* {@code Thread} also makes it easy to kill work that is in progress.
*/
public class LocalRepoService extends IntentService {
public static final String TAG = "LocalRepoService";
public static final String ACTION_CREATE = "org.fdroid.fdroid.nearby.action.CREATE";
public static final String EXTRA_PACKAGE_NAMES = "org.fdroid.fdroid.nearby.extra.PACKAGE_NAMES";
public static final String ACTION_STATUS = "localRepoStatusAction";
public static final String EXTRA_STATUS = "localRepoStatusExtra";
public static final int STATUS_STARTED = 0;
public static final int STATUS_PROGRESS = 1;
public static final int STATUS_ERROR = 2;
private String[] currentlyProcessedApps = new String[0];
private GenerateLocalRepoThread thread;
public LocalRepoService() {
super("LocalRepoService");
}
/**
* Creates a skeleton swap repo with only F-Droid itself in it
*/
public static void create(Context context) {
create(context, Collections.singleton(context.getPackageName()));
}
/**
* Sets up the local repo with the included {@code packageNames}
*/
public static void create(Context context, Set<String> packageNames) {
Intent intent = new Intent(context, LocalRepoService.class);
intent.setAction(ACTION_CREATE);
intent.putExtra(EXTRA_PACKAGE_NAMES, packageNames.toArray(new String[0]));
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
String[] packageNames = intent.getStringArrayExtra(EXTRA_PACKAGE_NAMES);
if (packageNames == null || packageNames.length == 0) {
Utils.debugLog(TAG, "no packageNames found, quiting");
return;
}
Arrays.sort(packageNames);
if (Arrays.equals(currentlyProcessedApps, packageNames)) {
Utils.debugLog(TAG, "packageNames list unchanged, quiting");
return;
}
currentlyProcessedApps = packageNames;
if (thread != null) {
thread.interrupt();
}
thread = new GenerateLocalRepoThread();
thread.start();
}
private class GenerateLocalRepoThread extends Thread {
private static final String TAG = "GenerateLocalRepoThread";
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
runProcess(LocalRepoService.this, currentlyProcessedApps);
}
}
public static void runProcess(Context context, String[] selectedApps) {
try {
final LocalRepoManager lrm = LocalRepoManager.get(context);
broadcast(context, STATUS_PROGRESS, R.string.deleting_repo);
lrm.deleteRepo();
for (String app : selectedApps) {
broadcast(context, STATUS_PROGRESS, context.getString(R.string.adding_apks_format, app));
lrm.addApp(context, app);
}
String urlString = Utils.getSharingUri(FDroidApp.repo).toString();
lrm.writeIndexPage(urlString);
broadcast(context, STATUS_PROGRESS, R.string.writing_index_jar);
lrm.writeIndexJar();
broadcast(context, STATUS_PROGRESS, R.string.linking_apks);
lrm.copyApksToRepo();
broadcast(context, STATUS_PROGRESS, R.string.copying_icons);
// run the icon copy without progress, its not a blocker
new Thread() {
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
lrm.copyIconsToRepo();
}
}.start();
broadcast(context, STATUS_STARTED, null);
} catch (IOException | XmlPullParserException | LocalRepoKeyStore.InitException e) {
broadcast(context, STATUS_ERROR, e.getLocalizedMessage());
e.printStackTrace();
}
}
/**
* Translate Android style broadcast {@link Intent}s to {@code PrepareSwapRepo}
*/
static void broadcast(Context context, int status, String message) {
Intent intent = new Intent(ACTION_STATUS);
intent.putExtra(EXTRA_STATUS, status);
if (message != null) {
intent.putExtra(Intent.EXTRA_TEXT, message);
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
static void broadcast(Context context, int status, int resId) {
broadcast(context, status, context.getString(resId));
}
}

View File

@ -1,248 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiConfiguration;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.peers.Peer;
import java.util.ArrayList;
import androidx.annotation.Nullable;
import com.google.android.material.switchmaterial.SwitchMaterial;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.mvdan.accesspoint.WifiApControl;
@SuppressWarnings("LineLength")
public class StartSwapView extends SwapView {
private static final String TAG = "StartSwapView";
public StartSwapView(Context context) {
super(context);
}
public StartSwapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
class PeopleNearbyAdapter extends ArrayAdapter<Peer> {
PeopleNearbyAdapter(Context context) {
super(context, 0, new ArrayList<Peer>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.swap_peer_list_item, parent, false);
}
Peer peer = getItem(position);
((TextView) convertView.findViewById(R.id.peer_name)).setText(peer.getName());
((ImageView) convertView.findViewById(R.id.icon))
.setImageDrawable(ContextCompat.getDrawable(getContext(), peer.getIcon()));
return convertView;
}
}
@Nullable /* Emulators typically don't have bluetooth adapters */
private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
private SwitchMaterial bluetoothSwitch;
private TextView viewBluetoothId;
private TextView textBluetoothVisible;
private TextView viewWifiId;
private TextView viewWifiNetwork;
private TextView peopleNearbyText;
private ListView peopleNearbyList;
private ProgressBar peopleNearbyProgress;
private PeopleNearbyAdapter peopleNearbyAdapter;
/**
* Remove relevant listeners/subscriptions/etc so that they do not receive and process events
* when this view is not in use.
* <p>
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (bluetoothSwitch != null) {
bluetoothSwitch.setOnCheckedChangeListener(null);
}
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiNetworkChanged);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
uiInitPeers();
uiInitBluetooth();
uiInitWifi();
uiInitButtons();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
onWifiNetworkChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
}
private final BroadcastReceiver onWifiNetworkChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
uiUpdateWifiNetwork();
}
};
private void uiInitButtons() {
findViewById(R.id.btn_send_fdroid).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getActivity().sendFDroid();
}
});
}
/**
* Setup the list of nearby peers with an adapter, and hide or show it and the associated
* message for when no peers are nearby depending on what is happening.
*/
private void uiInitPeers() {
peopleNearbyText = (TextView) findViewById(R.id.text_people_nearby);
peopleNearbyList = (ListView) findViewById(R.id.list_people_nearby);
peopleNearbyProgress = (ProgressBar) findViewById(R.id.searching_people_nearby);
peopleNearbyAdapter = new PeopleNearbyAdapter(getContext());
peopleNearbyList.setAdapter(peopleNearbyAdapter);
peopleNearbyAdapter.addAll(getActivity().getSwapService().getActivePeers());
peopleNearbyList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Peer peer = peopleNearbyAdapter.getItem(position);
onPeerSelected(peer);
}
});
}
private void uiShowNotSearchingForPeers() {
peopleNearbyProgress.setVisibility(View.GONE);
if (peopleNearbyList.getAdapter().getCount() > 0) {
peopleNearbyText.setText(getContext().getString(R.string.swap_people_nearby));
} else {
peopleNearbyText.setText(getContext().getString(R.string.swap_no_peers_nearby));
}
}
private void uiInitBluetooth() {
if (bluetooth != null) {
viewBluetoothId = (TextView) findViewById(R.id.device_id_bluetooth);
viewBluetoothId.setText(bluetooth.getName());
viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE);
textBluetoothVisible = findViewById(R.id.bluetooth_visible);
bluetoothSwitch = (SwitchMaterial) findViewById(R.id.switch_bluetooth);
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
bluetoothSwitch.setChecked(SwapService.getBluetoothVisibleUserPreference());
bluetoothSwitch.setEnabled(true);
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
} else {
findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
}
}
private final CompoundButton.OnCheckedChangeListener onBluetoothSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap, prompting user as to whether they want to enable Bluetooth.");
getActivity().startBluetoothSwap();
textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
viewBluetoothId.setVisibility(View.VISIBLE);
Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap (prompting user or setup Bluetooth complete)");
// TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
} else {
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, disabling Bluetooth swap.");
BluetoothManager.stop(getContext());
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
viewBluetoothId.setVisibility(View.GONE);
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully.");
}
SwapService.putBluetoothVisibleUserPreference(isChecked);
}
};
private void uiInitWifi() {
viewWifiId = (TextView) findViewById(R.id.device_id_wifi);
viewWifiNetwork = (TextView) findViewById(R.id.wifi_network);
uiUpdateWifiNetwork();
}
private void uiUpdateWifiNetwork() {
viewWifiId.setText(FDroidApp.ipAddressString);
viewWifiId.setVisibility(TextUtils.isEmpty(FDroidApp.ipAddressString) ? View.GONE : View.VISIBLE);
WifiApControl wifiAp = WifiApControl.getInstance(getActivity());
if (wifiAp != null && wifiAp.isWifiApEnabled()) {
WifiConfiguration config = wifiAp.getConfiguration();
TextView textWifiVisible = findViewById(R.id.wifi_visible);
if (textWifiVisible != null) {
textWifiVisible.setText(R.string.swap_visible_hotspot);
}
Context context = getContext();
if (config == null) {
viewWifiNetwork.setText(context.getString(R.string.swap_active_hotspot,
context.getString(R.string.swap_blank_wifi_ssid)));
} else {
viewWifiNetwork.setText(context.getString(R.string.swap_active_hotspot, config.SSID));
}
} else if (TextUtils.isEmpty(FDroidApp.ssid)) {
// not connected to or setup with any wifi network
viewWifiNetwork.setText(R.string.swap_no_wifi_network);
} else {
// connected to a regular wifi network
viewWifiNetwork.setText(FDroidApp.ssid);
}
}
private void onPeerSelected(Peer peer) {
getActivity().swapWith(peer);
}
}

View File

@ -1,93 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import org.fdroid.fdroid.R;
import androidx.annotation.ColorInt;
import androidx.annotation.LayoutRes;
import androidx.core.content.ContextCompat;
/**
* A {@link android.view.View} that registers to handle the swap events from
* {@link SwapService}.
*/
public class SwapView extends RelativeLayout {
public static final String TAG = "SwapView";
@ColorInt
public final int toolbarColor;
public final String toolbarTitle;
private int layoutResId = -1;
protected String currentFilterString;
public SwapView(Context context) {
this(context, null);
}
public SwapView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* In order to support Android < 21, this calls {@code super} rather than
* {@code this}. {@link RelativeLayout}'s methods just use a 0 for the
* fourth argument, just like this used to.
*/
public SwapView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SwapView, 0, 0);
toolbarColor = a.getColor(R.styleable.SwapView_toolbarColor,
ContextCompat.getColor(context, R.color.swap_blue));
toolbarTitle = a.getString(R.styleable.SwapView_toolbarTitle);
a.recycle();
}
@TargetApi(21)
public SwapView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SwapView, 0, 0);
toolbarColor = a.getColor(R.styleable.SwapView_toolbarColor,
ContextCompat.getColor(context, R.color.swap_blue));
toolbarTitle = a.getString(R.styleable.SwapView_toolbarTitle);
a.recycle();
}
@LayoutRes
public int getLayoutResId() {
return layoutResId;
}
public void setLayoutResId(@LayoutRes int layoutResId) {
this.layoutResId = layoutResId;
}
public String getCurrentFilterString() {
return this.currentFilterString;
}
public void setCurrentFilterString(String currentFilterString) {
this.currentFilterString = currentFilterString;
}
public SwapWorkflowActivity getActivity() {
return (SwapWorkflowActivity) getContext();
}
@ColorInt
public int getToolbarColour() {
return toolbarColor;
}
public String getToolbarTitle() {
return toolbarTitle;
}
}

View File

@ -1,97 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
/**
* @see <a href="https://stackoverflow.com/a/36162691">Android 5.0 DocumentFile from tree URI</a>
*/
public final class TreeUriUtils {
public static final String TAG = "TreeUriUtils";
private static final String PRIMARY_VOLUME_NAME = "primary";
@Nullable
public static String getFullPathFromTreeUri(Context context, @Nullable final Uri treeUri) {
if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), context);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
} else return volumePath;
}
@SuppressLint("ObsoleteSdkInt")
private static String getVolumePath(final String volumeId, Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
try {
StorageManager mStorageManager = ContextCompat.getSystemService(context, StorageManager.class);
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if (split.length > 0) return split[0];
else return null;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null)) return split[1];
else return File.separator;
}
}

View File

@ -1,77 +0,0 @@
/*
* Copyright (C) 2018-2019 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.nearby;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.UriPermission;
import android.database.ContentObserver;
import android.hardware.usb.UsbManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.views.main.NearbyViewBinder;
import androidx.annotation.RequiresApi;
/**
* This is just a shim to receive {@link UsbManager#ACTION_USB_ACCESSORY_ATTACHED}
* events.
*/
public class UsbDeviceAttachedReceiver extends BroadcastReceiver {
public static final String TAG = "UsbDeviceAttachedReceiv";
@RequiresApi(api = 19)
@Override
public void onReceive(final Context context, Intent intent) {
if (Build.VERSION.SDK_INT < 19) {
return;
}
if (intent == null || TextUtils.isEmpty(intent.getAction())
|| !UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
Log.i(TAG, "ignoring irrelevant intent: " + intent);
return;
}
Log.i(TAG, "handling intent: " + intent);
final ContentResolver contentResolver = context.getContentResolver();
for (final UriPermission uriPermission : contentResolver.getPersistedUriPermissions()) {
Uri uri = uriPermission.getUri();
final ContentObserver contentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
NearbyViewBinder.updateUsbOtg(context);
}
};
contentResolver.registerContentObserver(uri, true, contentObserver);
UsbDeviceDetachedReceiver.contentObservers.put(uri, contentObserver);
}
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2018-2019 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.nearby;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.hardware.usb.UsbManager;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.views.main.NearbyViewBinder;
import java.util.HashMap;
import androidx.annotation.RequiresApi;
/**
* This is just a shim to receive {@link UsbManager#ACTION_USB_DEVICE_DETACHED}
* events.
*/
public class UsbDeviceDetachedReceiver extends BroadcastReceiver {
public static final String TAG = "UsbDeviceDetachedReceiv";
static final HashMap<Uri, ContentObserver> contentObservers = new HashMap<>();
@RequiresApi(api = 19)
@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT < 19) {
return;
}
if (intent == null || TextUtils.isEmpty(intent.getAction())
|| !UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
Log.i(TAG, "ignoring irrelevant intent: " + intent);
return;
}
Log.i(TAG, "handling intent: " + intent);
final ContentResolver contentResolver = context.getContentResolver();
NearbyViewBinder.updateUsbOtg(context);
for (ContentObserver contentObserver : contentObservers.values()) {
contentResolver.unregisterContentObserver(contentObserver);
}
}
}

View File

@ -1,24 +0,0 @@
package org.fdroid.fdroid.nearby;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import org.fdroid.fdroid.views.main.NearbyViewBinder;
public class UsbDeviceMediaMountedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
String action = intent.getAction();
if (Environment.MEDIA_BAD_REMOVAL.equals(action)
|| Environment.MEDIA_MOUNTED.equals(action)
|| Environment.MEDIA_REMOVED.equals(action)
|| Environment.MEDIA_EJECTING.equals(action)) {
NearbyViewBinder.updateUsbOtg(context);
}
}
}

View File

@ -1,29 +0,0 @@
package org.fdroid.fdroid.nearby.peers;
import android.os.Parcelable;
import androidx.annotation.DrawableRes;
/**
* TODO This model assumes that "peers" from Bluetooth, Bonjour, and WiFi are
* different things. They are not different repos though, they all point to
* the same repos. This should really be combined to be a single "RemoteRepo"
* class that represents a single device's local repo, and can have zero to
* many ways to connect to it (e.g. Bluetooth, WiFi, USB Thumb Drive, SD Card,
* WiFi Direct, etc).
*/
public interface Peer extends Parcelable {
String getName();
@DrawableRes
int getIcon();
boolean equals(Object peer);
String getRepoAddress();
String getFingerprint();
boolean shouldPromptForSwapBack();
}

View File

@ -1,4 +1,4 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.net;
/*
* #%L
@ -35,9 +35,13 @@ package org.fdroid.fdroid.nearby;
import android.content.Context;
import android.net.Uri;
import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@ -45,7 +49,6 @@ import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -60,11 +63,6 @@ import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import javax.net.ssl.SSLServerSocketFactory;
import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
/**
* A HTTP server for serving the files that are being swapped via WiFi, etc.
* The only changes were to remove unneeded extras like {@code main()}, the
@ -82,7 +80,7 @@ public class LocalHTTPD extends NanoHTTPD {
*/
public static final String[] INDEX_FILE_NAMES = {"index.html"};
private final WeakReference<Context> context;
private final Context context;
protected List<File> rootDirs;
@ -103,7 +101,7 @@ public class LocalHTTPD extends NanoHTTPD {
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
super(hostname, port);
rootDirs = Collections.singletonList(webRoot);
this.context = new WeakReference<>(context.getApplicationContext());
this.context = context.getApplicationContext();
if (useHttps) {
enableHTTPS();
}
@ -372,7 +370,7 @@ public class LocalHTTPD extends NanoHTTPD {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
"Requires 'repo' parameter to be posted.");
}
SwapWorkflowActivity.requestSwap(context.get(), session.getParms().get("repo"));
SwapWorkflowActivity.requestSwap(context, session.getParms().get("repo"));
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
}
return newFixedLengthResponse("");
@ -493,7 +491,7 @@ public class LocalHTTPD extends NanoHTTPD {
private void enableHTTPS() {
try {
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context.get());
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
localRepoKeyStore.getKeyStore(),
localRepoKeyStore.getKeyManagers());

View File

@ -1,4 +1,4 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.net;
import android.app.IntentService;
import android.content.ComponentName;
@ -6,25 +6,22 @@ import android.content.Context;
import android.content.Intent;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.apache.commons.net.util.SubnetUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import java.net.Inet6Address;
import java.net.InetAddress;
@ -35,14 +32,11 @@ import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Locale;
import cc.mvdan.accesspoint.WifiApControl;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
/**
* Handle state changes to the device's wifi, storing the required bits.
* The {@link Intent} that starts it either has no extras included,
* which is how it can be triggered by code, or it came in from the system
* via {@link WifiStateChangeReceiver}, in
* via {@link org.fdroid.fdroid.receiver.WifiStateChangeReceiver}, in
* which case an instance of {@link NetworkInfo} is included.
* <p>
* The work is done in a {@link Thread} so that new incoming {@code Intents}
@ -65,14 +59,10 @@ public class WifiStateChangeService extends IntentService {
private static final String TAG = "WifiStateChangeService";
public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
public static final String EXTRA_STATUS = "wifiStateChangeStatus";
private WifiManager wifiManager;
private static WifiInfoThread wifiInfoThread;
private static int previousWifiState = Integer.MIN_VALUE;
private static int wifiState;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public WifiStateChangeService() {
super("WifiStateChangeService");
@ -86,12 +76,6 @@ public class WifiStateChangeService extends IntentService {
context.startService(intent);
}
@Override
public void onDestroy() {
compositeDisposable.dispose();
super.onDestroy();
}
@Override
protected void onHandleIntent(Intent intent) {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
@ -101,11 +85,10 @@ public class WifiStateChangeService extends IntentService {
}
Utils.debugLog(TAG, "WiFi change service started.");
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class);
wifiState = wifiManager.getWifiState();
wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
int wifiState = wifiManager.getWifiState();
if (ni == null || ni.isConnected()) {
Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState));
if (ni == null
|| ni.getState() == NetworkInfo.State.CONNECTED || ni.getState() == NetworkInfo.State.DISCONNECTED) {
if (previousWifiState != wifiState &&
(wifiState == WifiManager.WIFI_STATE_ENABLED
|| wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot
@ -119,12 +102,13 @@ public class WifiStateChangeService extends IntentService {
}
if (Build.VERSION.SDK_INT < 21 && wifiState == WifiManager.WIFI_STATE_ENABLED) {
compositeDisposable.add(UpdateService.scheduleIfStillOnWifi(this).subscribe());
UpdateService.scheduleIfStillOnWifi(this);
}
}
}
public class WifiInfoThread extends Thread {
private static final String TAG = "WifiInfoThread";
@Override
public void run() {
@ -143,7 +127,6 @@ public class WifiStateChangeService extends IntentService {
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
wifiInfo = wifiManager.getConnectionInfo();
FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress());
setSsid(wifiInfo);
DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
if (dhcpInfo != null) {
String netmask = formatIpAddress(dhcpInfo.netmask);
@ -161,13 +144,14 @@ public class WifiStateChangeService extends IntentService {
setIpInfoFromNetworkInterface();
}
} else if (wifiState == WifiManager.WIFI_STATE_DISABLED
|| wifiState == WifiManager.WIFI_STATE_DISABLING
|| wifiState == WifiManager.WIFI_STATE_UNKNOWN) {
|| wifiState == WifiManager.WIFI_STATE_DISABLING) {
// try once to see if its a hotspot
setIpInfoFromNetworkInterface();
if (FDroidApp.ipAddressString == null) {
return;
}
} else { // a hotspot can be active during WIFI_STATE_UNKNOWN
setIpInfoFromNetworkInterface();
}
if (retryCount > 120) {
@ -184,7 +168,17 @@ public class WifiStateChangeService extends IntentService {
return;
}
setSsid(wifiInfo);
if (wifiInfo != null) {
String ssid = wifiInfo.getSSID();
Utils.debugLog(TAG, "Have wifi info, connected to " + ssid);
if (ssid != null) {
FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
}
String bssid = wifiInfo.getBSSID();
if (bssid != null) {
FDroidApp.bssid = bssid;
}
}
String scheme;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
@ -234,56 +228,10 @@ public class WifiStateChangeService extends IntentService {
return;
}
Intent intent = new Intent(BROADCAST);
intent.putExtra(EXTRA_STATUS, wifiState);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
}
}
private void setSsid(WifiInfo wifiInfo) {
if (wifiInfo != null && wifiInfo.getBSSID() != null) {
String ssid = wifiInfo.getSSID();
Utils.debugLog(TAG, "Have wifi info, connected to " + ssid);
if (ssid == null) {
FDroidApp.ssid = getString(R.string.swap_blank_wifi_ssid);
} else {
FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
}
FDroidApp.bssid = wifiInfo.getBSSID();
} else {
WifiApControl wifiApControl = null;
try {
wifiApControl = WifiApControl.getInstance(this);
wifiApControl.isEnabled();
} catch (NullPointerException e) {
wifiApControl = null;
}
Utils.debugLog(TAG, "WifiApControl: " + wifiApControl);
if (wifiApControl == null && FDroidApp.ipAddressString != null) {
wifiInfo = wifiManager.getConnectionInfo();
if (wifiInfo != null && wifiInfo.getBSSID() != null) {
setSsid(wifiInfo);
} else {
FDroidApp.ssid = getString(R.string.swap_active_hotspot, "");
}
} else if (wifiApControl != null && wifiApControl.isEnabled()) {
WifiConfiguration wifiConfiguration = wifiApControl.getConfiguration();
Utils.debugLog(TAG, "WifiConfiguration: " + wifiConfiguration);
if (wifiConfiguration == null) {
FDroidApp.ssid = getString(R.string.swap_active_hotspot, "");
FDroidApp.bssid = "";
return;
}
if (wifiConfiguration.hiddenSSID) {
FDroidApp.ssid = getString(R.string.swap_hidden_wifi_ssid);
} else {
FDroidApp.ssid = wifiConfiguration.SSID;
}
FDroidApp.bssid = wifiConfiguration.BSSID;
}
}
}
/**
* Search for known Wi-Fi, Hotspot, and local network interfaces and get
* the IP Address info from it. This is necessary because network
@ -303,7 +251,7 @@ public class WifiStateChangeService extends IntentService {
while (networkInterfaces.hasMoreElements()) {
NetworkInterface netIf = networkInterfaces.nextElement();
for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements();) {
InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) {
continue;
@ -328,7 +276,7 @@ public class WifiStateChangeService extends IntentService {
if (BuildConfig.DEBUG) {
e.printStackTrace();
} else {
Log.i(TAG, "Getting subnet failed: " + e.getLocalizedMessage());
Log.i(TAG, e.getLocalizedMessage());
}
}
}

View File

@ -0,0 +1,68 @@
package org.fdroid.fdroid.net.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import java.io.IOException;
public class BluetoothClient {
@SuppressWarnings("unused")
private static final String TAG = "BluetoothClient";
private final BluetoothDevice device;
public BluetoothClient(BluetoothDevice device) {
this.device = device;
}
public BluetoothClient(String macAddress) {
device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
}
public BluetoothConnection openConnection() throws IOException {
BluetoothSocket socket = null;
BluetoothConnection connection = null;
try {
socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
connection = new BluetoothConnection(socket);
connection.open();
return connection;
} catch (IOException e1) {
if (connection != null) {
connection.closeQuietly();
}
throw e1;
/*
Log.e(TAG, "There was an error while establishing Bluetooth connection. Falling back to reflection");
Class<?> clazz = socket.getRemoteDevice().getClass();
Class<?>[] paramTypes = new Class<?>[]{Integer.TYPE};
Method method;
try {
method = clazz.getMethod("createInsecureRfcommSocket", paramTypes);
Object[] params = new Object[]{1};
BluetoothSocket sockFallback = (BluetoothSocket) method.invoke(socket.getRemoteDevice(), params);
BluetoothConnection connection = new BluetoothConnection(sockFallback);
connection.open();
return connection;
} catch (NoSuchMethodException e) {
throw e1;
} catch (IllegalAccessException e) {
throw e1;
} catch (InvocationTargetException e) {
throw e1;
}*/
// Don't catch exceptions this time, let it bubble up as we did our best but don't
// have anythign else to offer in terms of resolving the problem right now.
}
}
}

View File

@ -1,14 +1,15 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.net.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import android.webkit.MimeTypeMap;
import fi.iki.elonen.NanoHTTPD;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.httpish.Request;
import org.fdroid.fdroid.nearby.httpish.Response;
import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
import org.fdroid.fdroid.net.bluetooth.httpish.Request;
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
import java.io.File;
import java.io.FileInputStream;
@ -20,8 +21,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import fi.iki.elonen.NanoHTTPD;
/**
* Act as a layer on top of LocalHTTPD server, by forwarding requests served
* over bluetooth to that server.
@ -35,9 +34,18 @@ public class BluetoothServer extends Thread {
private final List<ClientConnection> clients = new ArrayList<>();
private final File webRoot;
private final BluetoothSwap swap;
private boolean isRunning;
public BluetoothServer(File webRoot) {
public BluetoothServer(BluetoothSwap swap, File webRoot) {
this.webRoot = webRoot;
this.swap = swap;
start();
}
public boolean isRunning() {
return isRunning;
}
public void close() {
@ -56,12 +64,15 @@ public class BluetoothServer extends Thread {
@Override
public void run() {
isRunning = true;
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
try {
serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
} catch (IOException e) {
Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now", e);
swap.stop();
isRunning = false;
return;
}
@ -91,6 +102,7 @@ public class BluetoothServer extends Thread {
Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients", e);
}
}
isRunning = false;
}
private static class ClientConnection extends Thread {

View File

@ -1,42 +0,0 @@
package org.fdroid.fdroid.panic;
import android.content.Context;
import android.util.AttributeSet;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import androidx.core.content.ContextCompat;
import androidx.preference.CheckBoxPreference;
import androidx.preference.PreferenceViewHolder;
public class DestructiveCheckBoxPreference extends CheckBoxPreference {
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DestructiveCheckBoxPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (!holder.itemView.isEnabled()) {
return;
}
if (FDroidApp.isAppThemeLight()) {
holder.itemView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.panic_destructive_light));
} else {
holder.itemView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.panic_destructive_dark));
}
}
}

View File

@ -1,39 +0,0 @@
package org.fdroid.fdroid.panic;
import android.content.Context;
import android.util.AttributeSet;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
public class DestructivePreference extends Preference {
public DestructivePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public DestructivePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public DestructivePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DestructivePreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (FDroidApp.isAppThemeLight()) {
holder.itemView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.panic_destructive_light));
} else {
holder.itemView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.panic_destructive_dark));
}
}
}

View File

@ -1,32 +0,0 @@
package org.fdroid.fdroid.panic;
import android.os.Bundle;
import android.view.View;
import com.google.android.material.appbar.MaterialToolbar;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import androidx.appcompat.app.AppCompatActivity;
public class PanicPreferencesActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle bundle) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
super.onCreate(bundle);
setContentView(R.layout.activity_panic_settings);
MaterialToolbar toolbar = findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Handle navigation icon press
onBackPressed();
}
});
}
}

View File

@ -1,174 +0,0 @@
package org.fdroid.fdroid.panic;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.InstalledApp;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import info.guardianproject.panic.Panic;
import info.guardianproject.panic.PanicResponder;
/**
* This {@link AppCompatActivity} is purely to run events in response to a panic trigger.
* It needs to be an {@code AppCompatActivity} rather than a {@link android.app.Service}
* so that it can fetch some of the required information about what sent the
* {@link Intent}. This is therefore an {@code AppCompatActivity} without any UI, which
* is a special case in Android. All the code must be in
* {@link #onCreate(Bundle)} and {@link #finish()} must be called at the end of
* that method.
*
* @see PanicResponder#receivedTriggerFromConnectedApp(AppCompatActivity)
*/
public class PanicResponderActivity extends AppCompatActivity {
private static final String TAG = PanicResponderActivity.class.getSimpleName();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (!Panic.isTriggerIntent(intent)) {
finish();
return;
}
// received intent from panic app
Log.i(TAG, "Received Panic Trigger...");
final Preferences preferences = Preferences.get();
boolean receivedTriggerFromConnectedApp = PanicResponder.receivedTriggerFromConnectedApp(this);
final boolean runningAppUninstalls = PrivilegedInstaller.isDefault(this);
ArrayList<String> wipeList = new ArrayList<>(preferences.getPanicWipeSet());
preferences.setPanicWipeSet(Collections.<String>emptySet());
preferences.setPanicTmpSelectedSet(Collections.<String>emptySet());
if (receivedTriggerFromConnectedApp && runningAppUninstalls && wipeList.size() > 0) {
// if this app (e.g. F-Droid) is to be deleted, do it last
if (wipeList.contains(getPackageName())) {
wipeList.remove(getPackageName());
wipeList.add(getPackageName());
}
final Context context = this;
final CountDownLatch latch = new CountDownLatch(1);
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
final String lastToUninstall = wipeList.get(wipeList.size() - 1);
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch ((intent.getAction())) {
case Installer.ACTION_UNINSTALL_INTERRUPTED:
case Installer.ACTION_UNINSTALL_COMPLETE:
latch.countDown();
break;
}
}
};
lbm.registerReceiver(receiver, Installer.getUninstallIntentFilter(lastToUninstall));
for (String packageName : wipeList) {
InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName);
InstallerService.uninstall(context, new Apk(installedApp));
}
// wait for apps to uninstall before triggering final responses
new Thread() {
@Override
public void run() {
try {
latch.await(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
// ignored
}
lbm.unregisterReceiver(receiver);
if (preferences.panicResetRepos()) {
resetRepos(context);
}
if (preferences.panicHide()) {
HidingManager.hide(context);
}
if (preferences.panicExit()) {
exitAndClear();
}
}
}.start();
} else if (receivedTriggerFromConnectedApp) {
if (preferences.panicResetRepos()) {
resetRepos(this);
}
// Performing destructive panic response
if (preferences.panicHide()) {
Log.i(TAG, "Hiding app...");
HidingManager.hide(this);
}
}
// exit and clear, if not deactivated
if (!runningAppUninstalls && preferences.panicExit()) {
exitAndClear();
}
finish();
}
static void resetRepos(Context context) {
HashSet<String> enabledAddresses = new HashSet<>();
HashSet<String> disabledAddresses = new HashSet<>();
String[] defaultReposItems = DBHelper.loadInitialRepos(context).toArray(new String[0]);
for (int i = 1; i < defaultReposItems.length; i += DBHelper.REPO_XML_ITEM_COUNT) {
if ("1".equals(defaultReposItems[i + 3])) {
enabledAddresses.add(defaultReposItems[i]);
} else {
disabledAddresses.add(defaultReposItems[i]);
}
}
List<Repo> repos = RepoProvider.Helper.all(context);
for (Repo repo : repos) {
ContentValues values = new ContentValues(1);
if (enabledAddresses.contains(repo.address)) {
values.put(Schema.RepoTable.Cols.IN_USE, true);
RepoProvider.Helper.update(context, repo, values);
} else if (disabledAddresses.contains(repo.address)) {
values.put(Schema.RepoTable.Cols.IN_USE, false);
RepoProvider.Helper.update(context, repo, values);
} else {
RepoProvider.Helper.remove(context, repo.getId());
}
}
}
private void exitAndClear() {
ExitActivity.exitAndRemoveFromRecentApps(this);
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
}
}
}

View File

@ -1,32 +0,0 @@
package org.fdroid.fdroid.panic;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.installed.InstalledAppListAdapter;
import org.fdroid.fdroid.views.installed.InstalledAppListItemController;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
public class SelectInstalledAppListAdapter extends InstalledAppListAdapter {
private final Set<String> selectedApps;
SelectInstalledAppListAdapter(AppCompatActivity activity) {
super(activity);
Preferences prefs = Preferences.get();
selectedApps = prefs.getPanicWipeSet();
prefs.setPanicTmpSelectedSet(selectedApps);
}
@NonNull
@Override
public InstalledAppListItemController onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false);
return new SelectInstalledAppListItemController(activity, view, selectedApps);
}
}

View File

@ -1,39 +0,0 @@
package org.fdroid.fdroid.panic;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemState;
import org.fdroid.fdroid.views.installed.InstalledAppListItemController;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
/**
* Shows the currently installed apps as a selectable list.
*/
public class SelectInstalledAppListItemController extends InstalledAppListItemController {
private final Set<String> selectedApps;
public SelectInstalledAppListItemController(AppCompatActivity activity, View itemView, Set<String> selectedApps) {
super(activity, itemView);
this.selectedApps = selectedApps;
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return new AppListItemState(app).setCheckBoxStatus(selectedApps.contains(app.packageName));
}
@Override
protected void onActionButtonPressed(App app) {
super.onActionButtonPressed(app);
}
}

View File

@ -1,133 +0,0 @@
/*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt
*
* 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.panic;
import android.annotation.SuppressLint;
import android.database.Cursor;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.google.android.material.appbar.MaterialToolbar;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.views.installed.InstalledAppListAdapter;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class SelectInstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
private InstalledAppListAdapter adapter;
private RecyclerView appList;
private TextView emptyState;
private int checkId;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.installed_apps_layout);
MaterialToolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.panic_add_apps_to_uninstall));
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
adapter = new SelectInstalledAppListAdapter(this);
appList = findViewById(R.id.app_list);
appList.setHasFixedSize(true);
appList.setLayoutManager(new LinearLayoutManager(this));
appList.setAdapter(adapter);
emptyState = findViewById(R.id.empty_state);
}
@Override
protected void onResume() {
super.onResume();
prefs = Preferences.get();
// Starts a new or restarts an existing Loader in this manager
getSupportLoaderManager().restartLoader(0, null, this);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, InstalledAppProvider.getAllAppsUri(), null, null, null, null);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
adapter.setApps(cursor);
if (adapter.getItemCount() == 0) {
appList.setVisibility(View.GONE);
emptyState.setVisibility(View.VISIBLE);
} else {
appList.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
adapter.setApps(null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem menuItem = menu.add(R.string.menu_select_for_wipe);
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
checkId = menuItem.getItemId();
menuItem.setIcon(R.drawable.check);
return true;
}
@SuppressLint("ApplySharedPref")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
finish();
if (item.getItemId() == checkId) {
prefs.setPanicWipeSet(prefs.getPanicTmpSelectedSet());
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -1,11 +1,11 @@
package org.fdroid.fdroid.nearby;
package org.fdroid.fdroid.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.WifiStateChangeService;
public class WifiStateChangeReceiver extends BroadcastReceiver {
private static final String TAG = "WifiStateChangeReceiver";

View File

@ -1,21 +1,18 @@
package org.fdroid.fdroid.panic;
package org.fdroid.fdroid.views.hiding;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.appbar.MaterialToolbar;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
/**
* A very hacky calculator which is barely functional.
* It is just meant to pass a very casual inspection.
@ -31,21 +28,17 @@ public class CalculatorActivity extends AppCompatActivity {
// unary operators
private static final String PERCENT = "%";
@Nullable
private String lastOp;
private @Nullable String lastOp;
// views
private TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calculator);
MaterialToolbar toolbar = findViewById(R.id.toolbar);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
textView = (TextView) findViewById(R.id.textView);

View File

@ -1,4 +1,4 @@
package org.fdroid.fdroid.panic;
package org.fdroid.fdroid.views.hiding;
import android.content.ComponentName;
import android.content.Context;
@ -7,14 +7,12 @@ import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.AlertDialog;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.main.MainActivity;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationManagerCompat;
/**
* This class is encapsulating all methods related to hiding the app from the launcher
* and restoring it.

View File

@ -3,44 +3,39 @@ package org.fdroid.fdroid.views.main;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.CategoryProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.panic.HidingManager;
import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.categories.CategoryAdapter;
import org.fdroid.fdroid.views.categories.CategoryController;
import org.fdroid.fdroid.views.hiding.HidingManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
/**
* Responsible for ensuring that the categories view is inflated and then populated correctly.
* Will start a loader to get the list of categories from the database and populate a recycler
* view with relevant info about each.
*/
class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
public static final String TAG = "CategoriesViewBinder";
private static final int LOADER_ID = 429820532;
@ -65,7 +60,6 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
final SwipeRefreshLayout swipeToRefresh =
(SwipeRefreshLayout) categoriesView.findViewById(R.id.swipe_to_refresh);
Utils.applySwipeLayoutColors(swipeToRefresh);
swipeToRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
@ -96,11 +90,10 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id != LOADER_ID) {
throw new IllegalArgumentException("id != LOADER_ID");
return null;
}
return new CursorLoader(
@ -115,7 +108,7 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
/**
* Reads all categories from the cursor and stores them in memory to provide to the {@link CategoryAdapter}.
* <p>
*
* It does this so it is easier to deal with localized/unlocalized categories without having
* to store the localized version in the database. It is not expected that the list of categories
* will grow so large as to make this a performance concern. If it does in the future, the

View File

@ -1,16 +1,14 @@
package org.fdroid.fdroid.views.main;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.PreferencesFragment;
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
/**
* Decides which view on the main screen to attach to a given {@link FrameLayout}. This class
* doesn't know which view it will be rendering at the time it is constructed. Rather, at some
@ -32,10 +30,10 @@ class MainViewController extends RecyclerView.ViewHolder {
}
/**
* @see LatestViewBinder
* @see WhatsNewViewBinder
*/
public void bindLatestView() {
new LatestViewBinder(activity, frame);
public void bindWhatsNewView() {
new WhatsNewViewBinder(activity, frame);
}
/**
@ -62,17 +60,8 @@ class MainViewController extends RecyclerView.ViewHolder {
new CategoriesViewBinder(activity, frame);
}
/**
* {@link android.os.Environment#isExternalStorageRemovable()} sometimes
* throughs {@link IllegalArgumentException}s when it can't find the
* storage.
*/
public void bindSwapView() {
try {
new NearbyViewBinder(activity, frame);
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
/**

View File

@ -1,19 +1,14 @@
package org.fdroid.fdroid.views.main;
import android.Manifest;
import android.content.Context;
import android.app.Activity;
import android.content.Intent;
import android.content.UriPermission;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.support.annotation.RequiresApi;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.widget.Button;
@ -21,20 +16,12 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.SDCardScannerService;
import org.fdroid.fdroid.nearby.SwapService;
import org.fdroid.fdroid.nearby.TreeUriScannerIntentService;
import org.fdroid.fdroid.localrepo.SDCardScannerService;
import org.fdroid.fdroid.localrepo.TreeUriScannerIntentService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.io.File;
import java.util.List;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
/**
* A splash screen encouraging people to start the swap process. The swap
@ -61,18 +48,15 @@ import androidx.core.content.ContextCompat;
* write access to the the removable storage.
*
* @see TreeUriScannerIntentService
* @see org.fdroid.fdroid.nearby.SDCardScannerService
* <p>
* TODO use {@link StorageManager#registerStorageVolumeCallback(Executor, StorageManager.StorageVolumeCallback)}
* @see org.fdroid.fdroid.localrepo.SDCardScannerService
*/
public class NearbyViewBinder {
class NearbyViewBinder {
public static final String TAG = "NearbyViewBinder";
private static File externalStorage = null;
private static View swapView;
static File externalStorage = null;
NearbyViewBinder(final AppCompatActivity activity, FrameLayout parent) {
swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, parent, true);
NearbyViewBinder(final Activity activity, FrameLayout parent) {
View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, parent, true);
TextView subtext = swapView.findViewById(R.id.both_parties_need_fdroid_text);
subtext.setText(activity.getString(R.string.nearby_splash__both_parties_need_fdroid,
@ -91,12 +75,14 @@ public class NearbyViewBinder {
ActivityCompat.requestPermissions(activity, new String[]{coarseLocation},
MainActivity.REQUEST_LOCATION_PERMISSIONS);
} else {
ContextCompat.startForegroundService(activity, new Intent(activity, SwapService.class));
activity.startActivity(new Intent(activity, SwapWorkflowActivity.class));
}
}
});
if (Build.VERSION.SDK_INT >= 21) {
Log.i(TAG, "Environment.isExternalStorageRemovable(activity.getExternalFilesDir(\"\")) " +
Environment.isExternalStorageRemovable(activity.getExternalFilesDir("")));
File[] dirs = activity.getExternalFilesDirs("");
if (dirs != null) {
for (File dir : dirs) {
@ -147,93 +133,5 @@ public class NearbyViewBinder {
}
});
}
updateUsbOtg(activity);
}
public static void updateUsbOtg(final Context context) {
if (Build.VERSION.SDK_INT < 24) {
return;
}
if (swapView == null) {
Utils.debugLog(TAG, "swapView == null");
return;
}
TextView storageVolumeText = swapView.findViewById(R.id.storage_volume_text);
Button requestStorageVolume = swapView.findViewById(R.id.request_storage_volume_button);
storageVolumeText.setVisibility(View.GONE);
requestStorageVolume.setVisibility(View.GONE);
final StorageManager storageManager = ContextCompat.getSystemService(context, StorageManager.class);
for (final StorageVolume storageVolume : storageManager.getStorageVolumes()) {
if (storageVolume.isRemovable() && !storageVolume.isPrimary()) {
Log.i(TAG, "StorageVolume: " + storageVolume);
Intent tmpIntent = null;
if (Build.VERSION.SDK_INT < 29) {
tmpIntent = storageVolume.createAccessIntent(null);
} else {
tmpIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
tmpIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
Uri.parse("content://"
+ TreeUriScannerIntentService.EXTERNAL_STORAGE_PROVIDER_AUTHORITY
+ "/tree/"
+ storageVolume.getUuid()
+ "%3A/document/"
+ storageVolume.getUuid()
+ "%3A"));
}
if (tmpIntent == null) {
Utils.debugLog(TAG, "Got null Storage Volume access Intent");
return;
}
final Intent intent = tmpIntent;
storageVolumeText.setVisibility(View.VISIBLE);
String text = storageVolume.getDescription(context);
if (!TextUtils.isEmpty(text)) {
requestStorageVolume.setText(text);
UsbDevice usb = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (usb != null) {
text = String.format("%s (%s %s)", text, usb.getManufacturerName(), usb.getProductName());
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
}
}
requestStorageVolume.setVisibility(View.VISIBLE);
requestStorageVolume.setOnClickListener(new View.OnClickListener() {
@Override
@RequiresApi(api = 24)
public void onClick(View v) {
List<UriPermission> list = context.getContentResolver().getPersistedUriPermissions();
if (list != null) for (UriPermission uriPermission : list) {
Uri uri = uriPermission.getUri();
if (uri.getPath().equals(String.format("/tree/%s:", storageVolume.getUuid()))) {
intent.setData(uri);
TreeUriScannerIntentService.onActivityResult(context, intent);
return;
}
}
AppCompatActivity activity = null;
if (context instanceof AppCompatActivity) {
activity = (AppCompatActivity) context;
} else if (swapView != null && swapView.getContext() instanceof AppCompatActivity) {
activity = (AppCompatActivity) swapView.getContext();
}
if (activity != null) {
activity.startActivityForResult(intent, MainActivity.REQUEST_STORAGE_ACCESS);
} else {
// scan in the background without requesting permissions
Toast.makeText(context.getApplicationContext(),
context.getString(R.string.scan_removable_storage_toast, externalStorage),
Toast.LENGTH_SHORT).show();
SDCardScannerService.scan(context);
}
}
});
}
}
}
}

View File

@ -1,11 +1,11 @@
package org.fdroid.fdroid.panic;
package org.fdroid.fdroid.views.panic;
import android.support.v7.app.AppCompatActivity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class ExitActivity extends AppCompatActivity {
@Override

View File

@ -0,0 +1,38 @@
package org.fdroid.fdroid.views.panic;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
public class PanicPreferencesActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle bundle) {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(bundle);
setContentView(R.layout.activity_panic_settings);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(true);
ab.setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}

Some files were not shown because too many files have changed in this diff Show More