Compare commits

..

No commits in common. "master" and "1.3-alpha3" have entirely different histories.

1815 changed files with 25128 additions and 50670 deletions

1
.gitattributes vendored
View File

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

3
.gitignore vendored
View File

@ -16,6 +16,7 @@ build.xml
# Gradle files # Gradle files
.gradle/ .gradle/
build/ build/
gradle.properties
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties
@ -46,4 +47,4 @@ junit-report.xml
# Screen dumps from Android Studio/DDMS # Screen dumps from Android Studio/DDMS
captures/ captures/
/fdroid/ /fdroid/

View File

@ -1,36 +1,25 @@
image: registry.gitlab.com/fdroid/ci-images-client:latest
cache:
paths:
- .gradle/wrapper
- .gradle/caches
stages: stages:
- test - test
- deploy - deploy
.base: before_script:
image: registry.gitlab.com/fdroid/ci-images-client:latest - export GRADLE_USER_HOME=$PWD/.gradle
before_script: - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- export GRADLE_USER_HOME=$PWD/.gradle - echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
- 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:
paths:
- .gradle/wrapper
- .gradle/caches
.test-template: &test-template .test-template: &test-template
extends: .base
stage: test stage: test
artifacts: artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}" name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths: paths:
- kernel.log
- logcat.txt - logcat.txt
- app/core*
- app/*.log
- app/build/reports - app/build/reports
- app/build/outputs/*ml - app/build/outputs/*ml
- app/build/outputs/apk - app/build/outputs/apk
@ -45,82 +34,56 @@ test_lint_pmd_checkstyle:
<<: *test-template <<: *test-template
script: script:
- export EXITVALUE=0 - 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 - ./gradlew assemble
# always report on lint errors to the build log # always report on lint errors to the build log
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle - sed -i -e 's,textReport .*,textReport true,' app/build.gradle
- ./gradlew testFullDebugUnitTest || set_error - ./gradlew test
- ./gradlew lint || set_error - ./gradlew lint
- ./gradlew pmd || set_error - ./gradlew pmd || export EXITVALUE=1
- ./gradlew checkstyle || set_error - ./gradlew checkstyle || export EXITVALUE=1
- ./tools/check-format-strings.py || set_error - ./tools/check-format-strings.py || export EXITVALUE=1
- ./tools/check-fastlane-whitespace.py || set_error - ./tools/remove-unused-and-blank-translations.py || export EXITVALUE=1
- ./tools/remove-unused-and-blank-translations.py || set_error
- echo "These are unused or blank translations that should be removed:" - 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 - exit $EXITVALUE
errorprone: errorprone:
extends: .base
stage: test stage: test
script: 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 - cat config/errorprone.gradle >> app/build.gradle
- ./gradlew -Dorg.gradle.dependency.verification=lenient assembleDebug - ./gradlew assembleDebug
allow_failure: true
# 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 .connected-template: &connected-template
extends: .base
script: script:
- ./gradlew assembleFullDebug - ./gradlew assembleDebug
- export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'` - echo y | sdkmanager "platforms;android-$AVD_SDK" > /dev/null
- export AVD_TAG=`echo $CI_JOB_NAME | awk '{print $3}'` - if ! avdmanager list avd | grep "Name. avd$AVD_SDK$"; then
- export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'` rm -rf ~/.android/avd $ANDROID_HOME/system-images;
- export AVD_PACKAGE="system-images;android-${AVD_SDK};${AVD_TAG};${AVD_ARCH}" echo y | sdkmanager "$AVD_PACKAGE" > /dev/null;
- echo $AVD_PACKAGE echo no | avdmanager create avd --name avd$AVD_SDK --tag "$AVD_TAG" --package "$AVD_PACKAGE";
avdmanager list avd;
- alias sdkmanager
- ls -l ~/.android
- adb start-server
- start-emulator
- 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 fi
- ./gradlew connectedFullDebugAndroidTest $FLAG - emulator64-arm -avd avd$AVD_SDK -no-audio -no-window -no-snapstorage &
- wait-for-emulator
- adb shell input keyevent 82 &
- ./gradlew connectedCheck || (adb -e logcat -d '*:E' > logcat.txt; exit 1)
no-accel 22 default x86: connected24:
<<: *test-template
<<: *connected-template
.kvm-template: &kvm-template
tags:
- fdroid
- kvm
only: only:
variables: - fdroid/fdroidclient@master
- $RUN_KVM_JOBS
<<: *test-template <<: *test-template
variables:
AVD_SDK: "24"
AVD_TAG: "default"
AVD_PACKAGE: "system-images;android-${AVD_SDK};${AVD_TAG};armeabi-v7a"
<<: *connected-template <<: *connected-template
kvm 29 microg x86_64: connected25:
<<: *kvm-template <<: *test-template
<<: *connected-template
deploy_nightly: deploy_nightly:
extends: .base
stage: deploy stage: deploy
only: only:
- master - master
@ -140,3 +103,8 @@ deploy_nightly:
# build the APKs! # build the APKs!
- ./gradlew assembleDebug - ./gradlew assembleDebug
- fdroid nightly -v - 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

@ -10,7 +10,7 @@ fdroid_root := $(LOCAL_PATH)
fdroid_dir := app fdroid_dir := app
fdroid_out := $(PWD)/$(OUT_DIR)/target/common/obj/APPS/$(LOCAL_MODULE)_intermediates fdroid_out := $(PWD)/$(OUT_DIR)/target/common/obj/APPS/$(LOCAL_MODULE)_intermediates
fdroid_build := $(fdroid_root)/$(fdroid_dir)/build fdroid_build := $(fdroid_root)/$(fdroid_dir)/build
fdroid_apk := build/outputs/apk/full/release/$(fdroid_dir)-full-release-unsigned.apk fdroid_apk := build/outputs/apk/$(fdroid_dir)-release-unsigned.apk
$(fdroid_root)/$(fdroid_dir)/$(fdroid_apk): $(fdroid_root)/$(fdroid_dir)/$(fdroid_apk):
rm -Rf $(fdroid_build) rm -Rf $(fdroid_build)

View File

@ -1,343 +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)
* choose random mirror for each package/APK download
* make all APK downloads be cached per-repo, not per-mirror
* handle Apache and Nginx ETags when checking if index is current (#1708)
### 1.6-alpha0 (2019-02-15)
* handle implied READ_EXTERNAL_STORAGE permissions, which trigger a
permissions prompt on installs with Privileged Extension (#1702)
* sanitize index data to reduce the threats from the server
* set Read Timeout to trigger mirror use when reads are slow
* fix missing icons for those who do not use WiFi (#1592)
* use separate titles for Updates pref and Updates tab, so that they
can be better translated
* UI fixes from @ConnyDuck (#1636, #1618)
### 1.5.1 (2019-01-07)
* Removed incomplete translations that were accidentally added in 1.5
* Fix screenshot background on dark themes (#1618)
### 1.5 (2018-12-26)
* Nearby swap bug fixes and improvements
* update language and translations about Nearby and swap
* Fix displaying of icons for self-built apps (#1108)
### 1.5-alpha2 (2018-12-21)
* support swapping via SD Cards
* display versionCode in expanded Versions list entries in Expert Mode
### 1.5-alpha1 (2018-12-12)
* UX and language cleanup of App Details
### 1.5-alpha0 (2018-10-19)
* add repos via additional_repos.xml from ROM, OEM, Vendor.
### 1.4 (2018-09-12)
* polish up new "Versions" list and other UI fixes
### 1.4-alpha1 (2018-08-30)
* huge overhaul of the "Versions" list in the App Details screen, and
many other UI improvements, thanks to new contributor @wsdfhjxc
* fixes to allow keyboard/d-pad navigation in more places, thanks to
new contributor @doeffinger
### 1.4-alpha0 (2018-08-17)
* show "Open" button when media is installed and viewable
* retry index downloads from mirrors
* add Share button to "Installed Apps" to export CSV list
* add clickable list of APKs to the swap HTML index page
### 1.3.1 (2018-08-07)
* big overhaul of core nearby/swap plumbing
* TLSv1.3 support, when the device supports it
### 1.3 (2018-07-31)
* large overhaul to make status updates more reliable
* fixed many bugs around the wrong button showing
### 1.3-alpha5 (2018-07-21)
* overhaul install button logic to avoid false presses
* improved first time run experience
* export install/uninstall history
* more whitelabeling improvements
### 1.3-alpha4 (2018-07-13)
* fix Data/WiFi preferences to properly schedule Updats
* fix Install/Uninstall events for clearer feedback
* track pending installs properly, stop fake repeating updates
* add support for Repo Push Requests when using Index V1
* support NoSourceSince anti-feature
* share menu item for repos
* fix a few crasher bugs
### 1.3-alpha3 (2018-06-27) ### 1.3-alpha3 (2018-06-27)
* fix bug that disabled Privileged Extension * fix bug that disabled Privileged Extension

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. 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 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 suggestions on how to improve them, open a merge request like you would if you
would if you were making code changes. This way the changes can be reviewed were making code changes. This way the changes can be reviewed before the
before the source strings on Weblate are changed. source strings on Weblate are changed.
## Code Style ## Code Style
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This We follow the [Android Java style](https://source.android.com/source/code-style.html).
should be more or less the same as [Android Java Some key points:
style](https://source.android.com/source/code-style.html). Some key points:
* Four space indentation * Four space indentation
* UTF-8 source files * 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 * Braces are always used after if, for and while
The current code base doesn't follow it entirely, but new code should follow 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 ## 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 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 connected via `adb`, or an emulator running. Then, execute the following from the
command line: command line:
./gradlew check ./gradlew check
Many important tests require a device or emulator, but do not work in GitLab CI. Note that the CI already runs the tests on an emulator, so you don't
That mean they need to be run locally, and that is usually easiest in Android necessarily have to do this yourself if you open a merge request as the tests
Studio rather than the command line. 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 \ Each stable version follows the `X.Y` pattern. Hotfix releases - i.e. when a
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest 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 # 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/) [![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 Client for [F-Droid](https://f-droid.org), the Free Software repository system
@ -38,7 +38,9 @@ to what Google Play does.
This used to be the case, but no longer is. Now the [Privileged This used to be the case, but no longer is. Now the [Privileged
Extension](https://gitlab.com/fdroid/privileged-extension) is the one that should be placed in Extension](https://gitlab.com/fdroid/privileged-extension) is the one that should be placed in
the system. It can be bundled with a ROM or installed via a zip. the system. It can be bundled with a ROM or installed via a zip, or
alternatively F-Droid can install it as a system app using root.
## License ## License
This program is Free Software: You can use, study share and improve it at your This program is Free Software: You can use, study share and improve it at your

23
RELEASE_CHECKLIST.md Normal file
View File

@ -0,0 +1,23 @@
# Release Checklist
This is the things that need to happen for all releases, alpha or stable:
* pull translations from Weblate: ./tools/pull-trans.sh
* rebase Weblate in its web interface, since we squash commits
* update `versionCode` in _app/build.gradle_
* make signed tag with version name
* update _metadata/org.fdroid.fdroid.txt_ in _fdroiddata_
## Stable releases
For stable releases, there are a couple more steps to do __before__
making the release tag:
* update CHANGELOG.md
* run `./tools/trim-incomplete-translations-for-release.py`

View File

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'checkstyle' apply plugin: 'checkstyle'
apply plugin: 'pmd' 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 getVersionName = { ->
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()
exec { exec {
@ -12,33 +12,20 @@ def getVersionName = { ->
return stdout.toString().trim() return stdout.toString().trim()
} }
def isCi = "true" == System.getenv("CI") def isCi = "true".equals(System.getenv("CI"))
def preDexEnabled = "true" == System.getProperty("pre-dex", "true") def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
def fullApplicationId = "org.fdroid.fdroid" def fullApplicationId = "org.fdroid.fdroid"
def basicApplicationId = "org.fdroid.basic" def basicApplicationId = "org.fdroid.basic"
// yes, this actually needs both quotes https://stackoverflow.com/a/41391841
def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
android { android {
compileSdkVersion 30 compileSdkVersion 27
buildToolsVersion '27.0.3'
defaultConfig { defaultConfig {
versionCode 1013001 versionCode 1003003
versionName getVersionName() versionName getVersionName()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
minSdkVersion 24
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 28
/*
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
under test for each invocation. If you do not wish to upload this data, you can opt-out by
passing the following argument to the test runner: disableAnalytics "true".
*/
testInstrumentationRunnerArguments disableAnalytics: 'true'
vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
@ -46,9 +33,8 @@ android {
// release builds before. // release builds before.
all { all {
minifyEnabled true minifyEnabled true
useProguard true
shrinkResources 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' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro' testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
} }
@ -76,8 +62,6 @@ android {
compileOptions { compileOptions {
compileOptions.encoding = "UTF-8" compileOptions.encoding = "UTF-8"
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
aaptOptions { aaptOptions {
@ -102,10 +86,6 @@ android {
events "skipped", "failed", "standardOut", "standardError" events "skipped", "failed", "standardOut", "standardError"
showStandardStreams = true showStandardStreams = true
} }
systemProperty 'robolectric.dependency.repo.url', 'https://repo1.maven.org/maven2'
// hack to avoid memory leak crashes
forkEvery = 1
} }
} }
} }
@ -142,56 +122,55 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.android.support:support-v4:27.1.1'
implementation 'androidx.preference:preference:1.1.1' implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'com.android.support:gridlayout-v7:27.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'com.android.support:support-annotations:27.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.android.support:design:27.1.1'
implementation 'androidx.palette:palette:1.0.0' implementation 'com.android.support:support-vector-drawable:27.1.1'
implementation 'androidx.work:work-runtime:2.4.0' implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.android.support:palette-v7:27.1.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.android.support:preference-v14:27.1.1'
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.2'
implementation 'info.guardianproject.netcipher:netcipher:2.2.0-alpha' implementation 'eu.chainfire:libsuperuser:1.0.0.201602271131'
implementation 'info.guardianproject.panic:panic:1.0' implementation 'info.guardianproject.netcipher:netcipher:2.0.0-alpha1'
implementation 'commons-io:commons-io:2.6' implementation 'info.guardianproject.panic:panic:0.5'
implementation 'commons-net:commons-net:3.6' implementation 'commons-io:commons-io:2.5'
implementation 'commons-net:commons-net:3.5'
implementation 'ch.acra:acra:4.9.1' 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.hannesdorfmann:adapterdelegates3:3.0.1'
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.4'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' implementation 'com.fasterxml.jackson.core:jackson-core:2.8.7'
implementation 'io.reactivex.rxjava3:rxjava:3.0.9' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.8.7'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.7'
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1' implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1' fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0' fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
fullImplementation 'org.jmdns:jmdns:3.5.5' fullImplementation 'org.jmdns:jmdns:3.5.3'
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1' fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
testImplementation 'androidx.test:core:1.3.0' testImplementation 'org.robolectric:robolectric:3.8'
testImplementation 'junit:junit:4.13.1' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59'
testImplementation 'org.robolectric:robolectric:4.3' testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'org.mockito:mockito-core:2.7.22'
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.65'
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0' androidTestImplementation 'com.android.support:support-annotations:25.3.1'
androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'com.android.support.test:runner:0.5'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'com.android.support.test:rules:0.5'
androidTestImplementation 'androidx.test:rules:1.3.0' }
androidTestImplementation 'androidx.test:monitor:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' tasks.whenTaskAdded { task ->
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' if (task.name.startsWith("lintBasic")) {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' android.lintOptions.abortOnError = false
androidTestImplementation 'androidx.work:work-testing:2.4.0' }
} }
checkstyle { checkstyle {
@ -207,7 +186,7 @@ task checkstyle(type: Checkstyle) {
} }
pmd { pmd {
toolVersion = '6.20.0' toolVersion = '5.5.1'
consoleOutput = true consoleOutput = true
} }
@ -217,6 +196,7 @@ task pmdMain(type: Pmd) {
ruleSets = [] // otherwise defaults clash with the list in rules.xml ruleSets = [] // otherwise defaults clash with the list in rules.xml
source 'src/main/java' source 'src/main/java'
include '**/*.java' include '**/*.java'
exclude '**/kellinwood/**/*.java'
} }
task pmdTest(type: Pmd) { task pmdTest(type: Pmd) {

View File

@ -9,39 +9,20 @@
<issue id="ImpliedQuantity" severity="error"/> <issue id="ImpliedQuantity" severity="error"/>
<issue id="DefaultLocale" severity="error"/> <issue id="DefaultLocale" severity="error"/>
<issue id="SimpleDateFormat" severity="error"/>
<issue id="NewApi" severity="error"/>
<issue id="InlinedApi" severity="error"/>
<!-- These are important to us, so promote from warning to error --> <!-- These are important to us, so promote from warning to error -->
<issue id="UnusedResources" severity="error"> <issue id="UnusedResources" severity="error">
<ignore path="src/main/res/drawable/category_**.png" /> <ignore path="src/main/res/drawable/category_**.png" />
<ignore path="src/main/res/values/dimens.xml"/> <ignore path="src/main/res/values/dimens.xml"/>
<ignore path="src/main/res/values/styles.xml"/> <ignore path="src/main/res/values/styles.xml"/>
<ignore path="src/full/res/values/styles.xml"/>
<!-- keep a single strings.xml for all build flavors -->
<ignore path="src/main/res/values**/strings.xml"/>
</issue> </issue>
<issue id="AppCompatMethod" severity="error"/> <issue id="AppCompatMethod" severity="error"/>
<issue id="NestedScrolling" severity="error"/> <issue id="NestedScrolling" severity="error"/>
<issue id="Typos" severity="error"/>
<issue id="StringFormatCount" severity="error"/> <issue id="StringFormatCount" severity="error"/>
<issue id="UnsafeProtectedBroadcastReceiver" severity="error"/> <issue id="UnsafeProtectedBroadcastReceiver" severity="error"/>
<issue id="GetInstance" severity="error"/>
<issue id="PackageManagerGetSignatures" severity="error"/>
<issue id="HardwareIds" severity="error"/>
<issue id="TrustAllX509TrustManager" severity="error">
<!-- these come from included libraries -->
<ignore path="org/apache/commons/net/ftp/FTPSTrustManager.class"/>
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$1.class"/>
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$2.class"/>
<ignore path="org/apache/commons/net/util/TrustManagerUtils$TrustManager.class"/>
</issue>
<issue id="PluralsCandidate" severity="error"/> <issue id="PluralsCandidate" severity="error"/>
<issue id="HardcodedText" 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 <!-- both the correct and deprecated locales need to be present for
them to be recognized on all devices --> them to be recognized on all devices -->
@ -55,7 +36,6 @@
</issue> </issue>
<issue id="ProtectedPermissions" severity="error"> <issue id="ProtectedPermissions" severity="error">
<ignore path="src/debug/AndroidManifest.xml"/>
<ignore path="src/full/AndroidManifest.xml"/> <ignore path="src/full/AndroidManifest.xml"/>
</issue> </issue>

View File

@ -4,16 +4,15 @@
-keep class org.fdroid.fdroid.** {*;} -keep class org.fdroid.fdroid.** {*;}
-dontskipnonpubliclibraryclassmembers -dontskipnonpubliclibraryclassmembers
-dontwarn android.test.** -dontwarn android.test.**
-dontwarn com.android.support.test.**
-dontwarn javax.naming.** -dontwarn javax.naming.**
-dontwarn org.slf4j.** -dontwarn org.slf4j.**
-dontnote org.apache.http.** -dontnote org.apache.http.**
-dontnote android.net.http.** -dontnote android.net.http.**
-dontnote android.support.**
-dontnote **ILicensingService -dontnote **ILicensingService
# Needed for espresso https://stackoverflow.com/a/21706087
-dontwarn org.xmlpull.v1.**
# StrongHttpsClient and its support classes are totally unused, so the # StrongHttpsClient and its support classes are totally unused, so the
# ch.boye.httpclientandroidlib.** classes are also unneeded # ch.boye.httpclientandroidlib.** classes are also unneeded
-dontwarn info.guardianproject.netcipher.client.** -dontwarn info.guardianproject.netcipher.client.**
@ -31,6 +30,24 @@
public *; 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 -keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** { *; } -keepnames class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.ext.** -dontwarn com.fasterxml.jackson.databind.ext.**
@ -39,9 +56,4 @@
public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; } public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; }
-keep public class your.class.** { -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,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us --> <!-- 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="org.fdroid.fdroid.tests"
package="org.fdroid.fdroid.tests" android:versionCode="1"
android:versionCode="1" android:versionName="1.0">
android:versionName="1.0">
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
<!-- We add an application tag here just so that we can indicate that <!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library, this package needs to link against the android.test library,
which is needed when building test cases. --> which is needed when building test cases. -->
<application> <application>
<uses-library <uses-library android:name="android.test.runner"/>
android:name="android.test.runner"
android:required="false" />
</application> </application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<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_EXTERNAL_STORAGE" /> <!--
This declares that this application uses the instrumentation test runner targeting
the package of org.fdroid.fdroid. To run the tests use the command:
"adb shell am instrument -w org.fdroid.fdroid.tests/android.test.InstrumentationTestRunner"
-->
<instrumentation android:name="com.zutubi.android.junitreport.JUnitReportTestRunner"
android:targetPackage="org.fdroid.fdroid"
android:label="Tests for org.fdroid.fdroid" />
</manifest> </manifest>

View File

@ -68,7 +68,7 @@
</permissions> </permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" /> <uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" /> <uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" /> <uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" /> <uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package> </package>
@ -85,7 +85,7 @@
<targetSdkVersion>23</targetSdkVersion> <targetSdkVersion>23</targetSdkVersion>
<added>2016-06-26</added> <added>2016-06-26</added>
<permissions> <permissions>
org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
</permissions> </permissions>
</package> </package>
<package> <package>

View File

@ -1,8 +1,8 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.content.Context; import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -16,9 +16,6 @@ public class AssetUtils {
private static final String TAG = "Utils"; private static final String TAG = "Utils";
/**
* This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
*/
@Nullable @Nullable
public static File copyAssetToDir(Context context, String assetName, File directory) { public static File copyAssetToDir(Context context, String assetName, File directory) {
File tempFile = null; File tempFile = null;
@ -31,7 +28,6 @@ public class AssetUtils {
output = new FileOutputStream(tempFile); output = new FileOutputStream(tempFile);
Utils.copy(input, output); Utils.copy(input, output);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Check the context is from Instrumentation.getContext()");
fail(e.getMessage()); fail(e.getMessage());
} finally { } finally {
Utils.closeQuietly(output); 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.AssetManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import androidx.test.platform.app.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
@ -179,9 +179,6 @@ public class LocalizationTest {
case "dd": case "dd":
resources.getString(resId, 1, 2); resources.getString(resId, 1, 2);
break; break;
case "ds":
resources.getString(resId, 1, "TWO");
break;
case "dds": case "dds":
resources.getString(resId, 1, 2, "THREE"); resources.getString(resId, 1, 2, "THREE");
break; break;

View File

@ -1,303 +0,0 @@
package org.fdroid.fdroid;
import android.Manifest;
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.util.Log;
import android.view.View;
import org.fdroid.fdroid.views.StatusBanner;
import org.fdroid.fdroid.views.main.MainActivity;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.swipeDown;
import static androidx.test.espresso.action.ViewActions.swipeLeft;
import static androidx.test.espresso.action.ViewActions.swipeRight;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.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";
/**
* Emulators older than {@code android-25} seem to fail at running Espresso tests.
* <p>
* ARM emulators are too slow to run these tests in a useful way. The sad
* thing is that it would probably work if Android didn't put up the ANR
* "Process system isn't responding" on boot each time. There seems to be no
* way to increase the ANR timeout.
*/
private static boolean canRunEspresso() {
if (Build.VERSION.SDK_INT < 25
|| (Build.SUPPORTED_ABIS[0].startsWith("arm") && isEmulator())) {
Log.e(TAG, "SKIPPING TEST: ARM emulators are too slow to run these tests in a useful way");
return false;
}
return true;
}
@BeforeClass
public static void classSetUp() {
IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.MINUTES);
IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.MINUTES);
if (!canRunEspresso()) {
return;
}
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
try {
UiDevice.getInstance(instrumentation)
.executeShellCommand("pm grant "
+ instrumentation.getTargetContext().getPackageName()
+ " android.permission.SET_ANIMATION_SCALE");
} catch (IOException e) {
e.printStackTrace();
}
SystemAnimations.disableAll(ApplicationProvider.getApplicationContext());
// dismiss the ANR or any other system dialogs that might be there
UiObject button = new UiObject(new UiSelector().text("Wait").enabled(true));
try {
button.click();
} catch (UiObjectNotFoundException e) {
Log.d(TAG, e.getLocalizedMessage());
}
new UiWatchers().registerAnrAndCrashWatchers();
Context context = instrumentation.getTargetContext();
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
ActivityManager activityManager = ContextCompat.getSystemService(context, ActivityManager.class);
activityManager.getMemoryInfo(mi);
long percentAvail = mi.availMem / mi.totalMem;
Log.i(TAG, "RAM: " + mi.availMem + " / " + mi.totalMem + " = " + percentAvail);
}
@AfterClass
public static void classTearDown() {
SystemAnimations.enableAll(ApplicationProvider.getApplicationContext());
}
public static boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk".equals(Build.PRODUCT);
}
@Before
public void setUp() {
assumeTrue(canRunEspresso());
}
/**
* Placate {@link android.os.StrictMode}
*
* @see <a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/6f3a38f3afd79ed6dddcef5c83cb442d6749e2ff"> Run finalizers before counting for StrictMode</a>
*/
@After
public void tearDown() {
System.gc();
System.runFinalization();
System.gc();
}
@Rule
public ActivityTestRule<MainActivity> activityTestRule =
new ActivityTestRule<>(MainActivity.class);
@Rule
public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant(
Manifest.permission.ACCESS_COARSE_LOCATION);
@Rule
public GrantPermissionRule writeExternalStoragePermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE);
@Test
public void bottomNavFlavorCheck() {
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
onView(withText(R.string.menu_settings)).check(matches(isDisplayed()));
onView(withText("THIS SHOULD NOT SHOW UP ANYWHERE!!!")).check(doesNotExist());
assertTrue(BuildConfig.FLAVOR.startsWith("full") || BuildConfig.FLAVOR.startsWith("basic"));
if (BuildConfig.FLAVOR.startsWith("basic")) {
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
onView(withText(R.string.main_menu__categories)).check(doesNotExist());
onView(withText(R.string.main_menu__swap_nearby)).check(doesNotExist());
}
if (BuildConfig.FLAVOR.startsWith("full")) {
onView(withText(R.string.main_menu__latest_apps)).check(matches(isDisplayed()));
onView(withText(R.string.main_menu__categories)).check(matches(isDisplayed()));
onView(withText(R.string.main_menu__swap_nearby)).check(matches(isDisplayed()));
}
}
@LargeTest
@Test
public void showSettings() {
ViewInteraction settingsBottonNavButton = onView(
allOf(withText(R.string.menu_settings), isDisplayed()));
settingsBottonNavButton.perform(click());
onView(withText(R.string.preference_manage_installed_apps)).check(matches(isDisplayed()));
if (BuildConfig.FLAVOR.startsWith("basic") && BuildConfig.APPLICATION_ID.endsWith(".debug")) {
// TODO fix me by sorting out the flavor applicationId for debug builds in app/build.gradle
Log.i(TAG, "Skipping the remainder of showSettings test because it just crashes on basic .debug builds");
return;
}
ViewInteraction manageInstalledAppsButton = onView(
allOf(withText(R.string.preference_manage_installed_apps), isDisplayed()));
manageInstalledAppsButton.perform(click());
onView(withText(R.string.installed_apps__activity_title)).check(matches(isDisplayed()));
onView(withContentDescription(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());
onView(withText(R.string.main_menu__updates)).check(matches(isDisplayed()));
}
@LargeTest
@Test
public void startSwap() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
}
ViewInteraction nearbyBottonNavButton = onView(
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()));
findPeopleButton.perform(click());
onView(withText(R.string.swap_send_fdroid)).check(matches(isDisplayed()));
}
@LargeTest
@Test
public void showCategories() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
}
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
onView(allOf(withText(R.string.main_menu__categories), isDisplayed())).perform(click());
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
.perform(swipeDown())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeDown())
.perform(swipeDown())
.perform(swipeRight())
.perform(swipeLeft())
.perform(swipeLeft())
.perform(swipeLeft())
.perform(swipeLeft())
.perform(click());
}
@LargeTest
@Test
public void showLatest() {
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
}
onView(Matchers.<View>instanceOf(StatusBanner.class)).check(matches(not(isDisplayed())));
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
onView(allOf(withId(R.id.swipe_to_refresh), isDisplayed()))
.perform(swipeDown())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeUp())
.perform(swipeDown())
.perform(swipeUp())
.perform(swipeDown())
.perform(swipeDown())
.perform(swipeDown())
.perform(swipeDown())
.perform(click());
}
@LargeTest
@Test
public void showSearch() {
onView(allOf(withText(R.string.menu_settings), isDisplayed())).perform(click());
onView(withId(R.id.fab_search)).check(doesNotExist());
if (!BuildConfig.FLAVOR.startsWith("full")) {
return;
}
onView(allOf(withText(R.string.main_menu__latest_apps), isDisplayed())).perform(click());
onView(allOf(withId(R.id.fab_search), isDisplayed())).perform(click());
onView(withId(R.id.sort)).check(matches(isDisplayed()));
onView(allOf(withId(R.id.search), isDisplayed()))
.perform(click())
.perform(typeText("test"));
onView(allOf(withId(R.id.sort), isDisplayed())).perform(click());
}
}

View File

@ -1,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

@ -1,62 +0,0 @@
package org.fdroid.fdroid;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.util.Log;
import java.lang.reflect.Method;
/**
* @see <a href="https://artemzin.com/blog/easiest-way-to-give-set_animation_scale-permission-for-your-ui-tests-on-android/>EASIEST WAY TO GIVE SET_ANIMATION_SCALE PERMISSION FOR YOUR UI TESTS ON ANDROID</a>
* @see <a href="https://gist.github.com/xrigau/11284124>Disable animations for Espresso tests</a>
*/
class SystemAnimations {
public static final String TAG = "SystemAnimations";
private static final float DISABLED = 0.0f;
private static final float DEFAULT = 1.0f;
static void disableAll(Context context) {
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
setSystemAnimationsScale(DISABLED);
} else {
Log.i(TAG, "Disabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
}
}
static void enableAll(Context context) {
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
setSystemAnimationsScale(DEFAULT);
} else {
Log.i(TAG, "Enabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
}
}
private static void setSystemAnimationsScale(float animationScale) {
try {
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
for (int i = 0; i < currentScales.length; i++) {
currentScales[i] = animationScale;
}
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
} catch (Exception e) {
Log.e(TAG, "Could not change animation scale to " + animationScale + " :'(");
}
}
}

View File

@ -1,156 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fdroid.fdroid;
import 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.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("MemberName")
public class UiWatchers {
private static final String LOG_TAG = UiWatchers.class.getSimpleName();
private final List<String> mErrors = new ArrayList<String>();
/**
* We can use the UiDevice registerWatcher to register a small script to be executed when the
* framework is waiting for a control to appear. Waiting may be the cause of an unexpected
* dialog on the screen and it is the time when the framework runs the registered watchers.
* This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
* create your own watchers and handle error logging properly for your type of tests.
*/
public void registerAnrAndCrashWatchers() {
UiDevice.getInstance().registerWatcher("ANR", new UiWatcher() {
@Override
public boolean checkForCondition() {
UiObject window = new UiObject(new UiSelector().className(
"com.android.server.am.AppNotRespondingDialog"));
String errorText = null;
if (window.exists()) {
try {
errorText = window.getText();
} catch (UiObjectNotFoundException e) {
Log.e(LOG_TAG, "dialog gone?", e);
}
onAnrDetected(errorText);
postHandler("Wait");
return true; // triggered
}
return false; // no trigger
}
});
// class names may have changed
UiDevice.getInstance().registerWatcher("ANR2", new UiWatcher() {
@Override
public boolean checkForCondition() {
UiObject window = new UiObject(new UiSelector().packageName("android")
.textContains("isn't responding."));
if (window.exists()) {
String errorText = null;
try {
errorText = window.getText();
} catch (UiObjectNotFoundException e) {
Log.e(LOG_TAG, "dialog gone?", e);
}
onAnrDetected(errorText);
postHandler("Wait");
return true; // triggered
}
return false; // no trigger
}
});
UiDevice.getInstance().registerWatcher("CRASH", new UiWatcher() {
@Override
public boolean checkForCondition() {
UiObject window = new UiObject(new UiSelector().className(
"com.android.server.am.AppErrorDialog"));
if (window.exists()) {
String errorText = null;
try {
errorText = window.getText();
} catch (UiObjectNotFoundException e) {
Log.e(LOG_TAG, "dialog gone?", e);
}
onCrashDetected(errorText);
postHandler("OK");
return true; // triggered
}
return false; // no trigger
}
});
UiDevice.getInstance().registerWatcher("CRASH2", new UiWatcher() {
@Override
public boolean checkForCondition() {
UiObject window = new UiObject(new UiSelector().packageName("android")
.textContains("has stopped"));
if (window.exists()) {
String errorText = null;
try {
errorText = window.getText();
} catch (UiObjectNotFoundException e) {
Log.e(LOG_TAG, "dialog gone?", e);
}
onCrashDetected(errorText);
postHandler("OK");
return true; // triggered
}
return false; // no trigger
}
});
Log.i(LOG_TAG, "Registered GUI Exception watchers");
}
public void onAnrDetected(String errorText) {
mErrors.add(errorText);
}
public void onCrashDetected(String errorText) {
mErrors.add(errorText);
}
public void reset() {
mErrors.clear();
}
public List<String> getErrors() {
return Collections.unmodifiableList(mErrors);
}
/**
* Current implementation ignores the exception and continues.
*/
public void postHandler(String buttonText) {
// TODO: Add custom error logging here
String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
.getInstance().getCurrentPackageName());
Log.e(LOG_TAG, formatedOutput);
UiObject buttonOK = new UiObject(new UiSelector().text(buttonText).enabled(true));
// sometimes it takes a while for the OK button to become enabled
buttonOK.waitForExists(5000);
try {
buttonOK.click();
} catch (UiObjectNotFoundException e) {
e.printStackTrace();
}
}
}

View File

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

View File

@ -22,16 +22,17 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.net.Uri; import android.net.Uri;
import android.os.Build; 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 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.AssetUtils;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest; import org.fdroid.fdroid.compat.FileCompatTest;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoXMLHandler;
import org.fdroid.fdroid.mock.RepoDetails; import org.fdroid.fdroid.mock.RepoDetails;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -44,7 +45,6 @@ import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.TreeSet;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -103,7 +103,7 @@ public class ApkVerifierTest {
public void testNulls() { public void testNulls() {
assertTrue(ApkVerifier.requestedPermissionsEqual(null, null)); assertTrue(ApkVerifier.requestedPermissionsEqual(null, null));
String[] perms = new String[]{"Blah"}; String[] perms = new String[] {"Blah"};
assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null)); assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null));
assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms)); assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms));
} }
@ -113,7 +113,7 @@ public class ApkVerifierTest {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
ArrayList<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList( String[] noPrefixPermissions = new String[]{
"AUTHENTICATE_ACCOUNTS", "AUTHENTICATE_ACCOUNTS",
"MANAGE_ACCOUNTS", "MANAGE_ACCOUNTS",
"READ_PROFILE", "READ_PROFILE",
@ -129,13 +129,8 @@ public class ApkVerifierTest {
"READ_SYNC_SETTINGS", "READ_SYNC_SETTINGS",
"WRITE_SYNC_SETTINGS", "WRITE_SYNC_SETTINGS",
"WRITE_CALL_LOG", // implied-permission! "WRITE_CALL_LOG", // implied-permission!
"READ_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]);
for (int i = 0; i < noPrefixPermissions.length; i++) { for (int i = 0; i < noPrefixPermissions.length; i++) {
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]); noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
} }
@ -182,7 +177,7 @@ public class ApkVerifierTest {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList( apk.requestedPermissions = new String[]{
"android.permission.AUTHENTICATE_ACCOUNTS", "android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS", "android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE", "android.permission.READ_PROFILE",
@ -198,12 +193,8 @@ public class ApkVerifierTest {
"android.permission.READ_SYNC_SETTINGS", "android.permission.READ_SYNC_SETTINGS",
"android.permission.WRITE_SYNC_SETTINGS", "android.permission.WRITE_SYNC_SETTINGS",
"android.permission.WRITE_CALL_LOG", // implied-permission! "android.permission.WRITE_CALL_LOG", // implied-permission!
"android.permission.READ_CALL_LOG"// implied-permission! "android.permission.READ_CALL_LOG", // implied-permission!
)); };
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
apk.requestedPermissions = expectedSet.toArray(new String[0]);
Uri uri = Uri.fromFile(sdk14Apk); Uri uri = Uri.fromFile(sdk14Apk);
@ -299,7 +290,7 @@ public class ApkVerifierTest {
public void testExtendedPerms() throws IOException, public void testExtendedPerms() throws IOException,
ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException { ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
RepoDetails actualDetails = getFromFile(extendedPermsXml); RepoDetails actualDetails = getFromFile(extendedPermsXml);
HashSet<String> expectedSet = new HashSet<>(Arrays.asList( HashSet<String> expectedSet = new HashSet<>(Arrays.asList(new String[]{
"android.permission.ACCESS_NETWORK_STATE", "android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE", "android.permission.ACCESS_WIFI_STATE",
"android.permission.INTERNET", "android.permission.INTERNET",
@ -310,8 +301,8 @@ public class ApkVerifierTest {
"android.permission.READ_CONTACTS", "android.permission.READ_CONTACTS",
"android.permission.WRITE_CONTACTS", "android.permission.WRITE_CONTACTS",
"android.permission.READ_CALENDAR", "android.permission.READ_CALENDAR",
"android.permission.WRITE_CALENDAR" "android.permission.WRITE_CALENDAR",
)); }));
if (Build.VERSION.SDK_INT <= 18) { if (Build.VERSION.SDK_INT <= 18) {
expectedSet.add("android.permission.READ_EXTERNAL_STORAGE"); expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE"); expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
@ -354,93 +345,6 @@ public class ApkVerifierTest {
apkVerifier.verifyApk(); apkVerifier.verifyApk();
} }
@Test
public void testImpliedPerms() throws IOException {
RepoDetails actualDetails = getFromFile(extendedPermsXml);
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE",
"android.permission.INTERNET",
"android.permission.READ_CALENDAR",
"android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.READ_SYNC_SETTINGS",
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
"android.permission.WRITE_CALENDAR",
"android.permission.WRITE_CONTACTS",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.WRITE_SYNC_SETTINGS",
"org.dmfs.permission.READ_TASKS",
"org.dmfs.permission.WRITE_TASKS"
));
if (Build.VERSION.SDK_INT <= 22) { // maxSdkVersion="22"
expectedSet.addAll(Arrays.asList(
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.GET_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS"
));
}
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
Apk apk = actualDetails.apks.get(1);
Log.i(TAG, "APK: " + apk.apkName);
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
for (String permission : expectedSet) {
if (!actualSet.contains(permission)) {
Log.i(TAG, permission + " in expected but not actual! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
for (String permission : actualSet) {
if (!expectedSet.contains(permission)) {
Log.i(TAG, permission + " in actual but not expected! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
expectedSet = new TreeSet<>(Arrays.asList(
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE",
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.GET_ACCOUNTS",
"android.permission.INTERNET",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_CALENDAR",
"android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.READ_SYNC_SETTINGS",
"android.permission.WRITE_CALENDAR",
"android.permission.WRITE_CONTACTS",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.WRITE_SYNC_SETTINGS",
"org.dmfs.permission.READ_TASKS",
"org.dmfs.permission.WRITE_TASKS"
));
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
apk = actualDetails.apks.get(2);
Log.i(TAG, "APK: " + apk.apkName);
actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
for (String permission : expectedSet) {
if (!actualSet.contains(permission)) {
Log.i(TAG, permission + " in expected but not actual! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
for (String permission : actualSet) {
if (!expectedSet.contains(permission)) {
Log.i(TAG, permission + " in actual but not expected! (android-"
+ Build.VERSION.SDK_INT + ")");
}
}
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
}
@NonNull @NonNull
private RepoDetails getFromFile(File indexFile) throws IOException { private RepoDetails getFromFile(File indexFile) throws IOException {
InputStream inputStream = null; InputStream inputStream = null;

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

@ -2,15 +2,11 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.util.Log;
import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.ProgressListener;
import org.junit.Test; import org.junit.Test;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -19,37 +15,22 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public class HttpDownloaderTest { public class HttpDownloaderTest {
private static final String TAG = "HttpDownloaderTest";
static final String[] URLS; String[] urls = {
"https://en.wikipedia.org/wiki/Index.html",
// https://developer.android.com/reference/javax/net/ssl/SSLContext "https://mirrors.kernel.org/debian/dists/stable/Release",
static { "https://f-droid.org/repo/index.jar",
ArrayList<String> tempUrls = new ArrayList<>(Arrays.asList( // sites that use SNI for HTTPS
"https://f-droid.org/repo/index-v1.jar", "https://guardianproject.info/fdroid/repo/index.jar",
// sites that use SNI for HTTPS //"https://microg.org/fdroid/repo/index.jar",
"https://mirrors.kernel.org/debian/dists/stable/Release", //"https://grobox.de/fdroid/repo/index.jar",
"https://fdroid.tetaneutral.net/fdroid/repo/index-v1.jar", };
"https://ftp.fau.de/fdroid/repo/index-v1.jar",
//"https://microg.org/fdroid/repo/index-v1.jar",
//"https://grobox.de/fdroid/repo/index.jar",
"https://guardianproject.info/fdroid/repo/index-v1.jar"
));
if (Build.VERSION.SDK_INT >= 22) {
tempUrls.addAll(Arrays.asList(
"https://en.wikipedia.org/wiki/Index.html", // no SNI but weird ipv6 lookup issues
"https://mirror.cyberbits.eu/fdroid/repo/index-v1.jar" // TLSv1.2 only and SNI
));
}
URLS = tempUrls.toArray(new String[tempUrls.size()]);
}
private boolean receivedProgress; private boolean receivedProgress;
@Test @Test
public void downloadUninterruptedTest() throws IOException, InterruptedException { public void downloadUninterruptedTest() throws IOException, InterruptedException {
for (String urlString : URLS) { for (String urlString : urls) {
Log.i(TAG, "URL: " + urlString);
Uri uri = Uri.parse(urlString); Uri uri = Uri.parse(urlString);
File destFile = File.createTempFile("dl-", ""); File destFile = File.createTempFile("dl-", "");
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile); HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
@ -70,7 +51,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile); final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() { httpDownloader.setListener(new ProgressListener() {
@Override @Override
public void onProgress(long bytesRead, long totalBytes) { public void onProgress(String urlString, long bytesRead, long totalBytes) {
receivedProgress = true; receivedProgress = true;
} }
}); });
@ -132,7 +113,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile); final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() { httpDownloader.setListener(new ProgressListener() {
@Override @Override
public void onProgress(long bytesRead, long totalBytes) { public void onProgress(String urlString, long bytesRead, long totalBytes) {
receivedProgress = true; receivedProgress = true;
latch.countDown(); 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.test.**
-dontwarn android.support.test.** -dontwarn android.support.test.**
-dontnote junit.framework.** -dontnote junit.framework.**
@ -18,8 +14,3 @@
-keep class junit.** { *; } -keep class junit.** { *; }
-dontwarn 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. * MA 02110-1301, USA.
*/ */
package org.fdroid.fdroid.nearby.peers; package org.fdroid.fdroid.localrepo.peers;
import org.fdroid.fdroid.data.NewRepoConfig; 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 SDCardScannerService {
public static void scan(Context context) {
}
}

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. * MA 02110-1301, USA.
*/ */
package org.fdroid.fdroid.nearby; package org.fdroid.fdroid.net;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.Nullable; import android.support.annotation.Nullable;
/** /**
* Dummy version for basic app flavor. * Dummy version for basic app flavor.

View File

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

View File

@ -17,7 +17,7 @@
* MA 02110-1301, USA. * MA 02110-1301, USA.
*/ */
package org.fdroid.fdroid.panic; package org.fdroid.fdroid.views.hiding;
import android.content.Context; import android.content.Context;
@ -29,9 +29,4 @@ public class HidingManager {
public static boolean isHidden(Context context) { public static boolean isHidden(Context context) {
return false; return false;
} }
public static void showHideDialog(final Context context) {
throw new IllegalStateException("unimplemented");
}
} }

View File

@ -19,13 +19,13 @@
package org.fdroid.fdroid.views.main; 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 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.R;
import org.fdroid.fdroid.views.PreferencesFragment; import org.fdroid.fdroid.views.fragments.PreferencesFragment;
import org.fdroid.fdroid.views.updates.UpdatesViewBinder; import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
/** /**
@ -48,11 +48,8 @@ class MainViewController extends RecyclerView.ViewHolder {
this.frame = frame; this.frame = frame;
} }
/** public void bindWhatsNewView() {
* @see LatestViewBinder throw new IllegalStateException("unimplemented");
*/
public void bindLatestView() {
new LatestViewBinder(activity, frame);
} }
/** /**

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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 -->
<item <item
app:showAsAction="ifRoom|withText" android:title="@string/updates"
android:id="@+id/latest"/> android:icon="@drawable/ic_updates"
app:showAsAction="ifRoom|withText"
android:id="@+id/updates" />
<item <item
app:showAsAction="ifRoom|withText" android:title="@string/menu_settings"
android:id="@+id/updates"/> android:icon="@drawable/ic_settings"
<item app:showAsAction="ifRoom|withText"
app:showAsAction="ifRoom|withText" android:id="@+id/settings" />
android:id="@+id/settings"/>
</menu> </menu>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">F-Droid Basic</string>
<string name="about_title">About F-Droid Basic</string>
</resources>

View File

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

View File

@ -1,8 +0,0 @@
<?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" />
</manifest>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
* Copyright (C) 2010-2012 Ciaran Gultnieks * Copyright (C) 2010-2012 Ciaran Gultnieks
* Copyright (C) 2013-2017 Peter Serwylo * Copyright (C) 2013-2017 Peter Serwylo
@ -24,155 +23,108 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fdroid.fdroid" package="org.fdroid.fdroid"
android:installLocation="auto"> android:installLocation="auto">
<uses-feature <uses-feature android:name="android.hardware.nfc" android:required="false"/>
android:name="android.hardware.nfc" <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature <uses-permission android:name="android.permission.INTERNET"/>
android:name="android.hardware.usb.host" <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
android:required="false" /> <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.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" /> <!-- Indicate that F-Droid may request root access (introduced by Koush's Superuser app)
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> This permission is deprecated, but necessary for some old superuser
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> apps to actually grant superuser access to F-Droid. -->
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
<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" />
<application> <application>
<activity <activity
android:name=".nearby.SwapWorkflowActivity" android:label="@string/swap"
android:configChanges="orientation|keyboardHidden" android:name=".views.swap.SwapWorkflowActivity"
android:label="@string/swap" android:parentActivityName=".views.main.MainActivity"
android:launchMode="singleTask" android:theme="@style/SwapTheme.Wizard"
android:parentActivityName=".views.main.MainActivity" android:screenOrientation="portrait"
android:screenOrientation="portrait"> android:configChanges="orientation|keyboardHidden">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".views.main.MainActivity" /> android:value=".views.main.MainActivity"/>
</activity> </activity>
<activity <receiver android:name=".receiver.WifiStateChangeReceiver">
android:name=".panic.PanicPreferencesActivity"
android:label="@string/panic_settings"
android:parentActivityName=".views.main.MainActivity">
<intent-filter> <intent-filter>
<action android:name="info.guardianproject.panic.action.CONNECT" /> <action android:name="android.net.wifi.STATE_CHANGE"/>
<action android:name="info.guardianproject.panic.action.DISCONNECT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> </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"/>
<activity
android:name=".views.panic.PanicPreferencesActivity"
android:label="@string/panic_settings"
android:parentActivityName=".views.main.MainActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" 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>
<activity <activity
android:name=".panic.SelectInstalledAppsActivity" android:name=".views.panic.PanicResponderActivity"
android:parentActivityName=".panic.PanicPreferencesActivity" /> android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<activity
android:name=".panic.PanicResponderActivity"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<!-- this can never have launchMode singleTask or singleInstance! --> <!-- this can never have launchMode singleTask or singleInstance! -->
<intent-filter> <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> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".panic.ExitActivity" android:name=".views.panic.ExitActivity"
android:theme="@android:style/Theme.NoDisplay" /> android:theme="@android:style/Theme.NoDisplay"/>
<activity <activity
android:name=".panic.CalculatorActivity" android:name=".views.hiding.CalculatorActivity"
android:enabled="false" android:enabled="false"
android:icon="@mipmap/ic_calculator_launcher" android:icon="@mipmap/ic_calculator_launcher"
android:label="@string/hiding_calculator"> android:label="@string/hiding_calculator"
android:theme="@style/AppThemeLight">
<intent-filter> <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> </intent-filter>
</activity> </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> </application>

View File

@ -4,13 +4,12 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.TextUtils; import android.text.TextUtils;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.util.ByteWrangler;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.UnknownHostException; 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. * 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 * In order to make it Parcelable (or Serializable for that matter), there are some package-scope

View File

@ -18,13 +18,12 @@ package kellinwood.logging;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale;
public abstract class AbstractLogger implements LoggerInterface { public abstract class AbstractLogger implements LoggerInterface {
protected String category; protected String category;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public AbstractLogger(String category) { public AbstractLogger(String category) {
this.category = category; this.category = category;

View File

@ -113,7 +113,7 @@ public class KeyStoreFileManager {
File keystoreFile = new File(keystorePath); File keystoreFile = new File(keystorePath);
try { try {
if (keystoreFile.exists()) { 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. // 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()); File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
FileOutputStream fos = new FileOutputStream(tmpFile); FileOutputStream fos = new FileOutputStream(tmpFile);

View File

@ -65,13 +65,6 @@ public class PasswordObfuscator {
return result; return result;
} }
/**
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
*
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
*/
@Deprecated
@SuppressWarnings("GetInstance")
public String encode(String junk, char[] password) { public String encode(String junk, char[] password) {
if (password == null) return null; if (password == null) return null;
try { try {
@ -91,13 +84,6 @@ public class PasswordObfuscator {
return null; return null;
} }
/**
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
*
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
*/
@Deprecated
@SuppressWarnings("GetInstance")
public char[] decode(String junk, String password) { public char[] decode(String junk, String password) {
if (password == null) return null; if (password == null) return null;
try { try {

View File

@ -38,10 +38,10 @@ public class SignatureBlockGenerator {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("BC"); JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("SC");
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey()); ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC"); JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("SC");
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build(); DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider); JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);

View File

@ -28,25 +28,18 @@ public class ZioEntryOutputStream extends OutputStream {
int crcValue = 0; int crcValue = 0;
OutputStream wrapped; OutputStream wrapped;
OutputStream downstream; OutputStream downstream;
Deflater deflater;
public ZioEntryOutputStream(int compression, OutputStream wrapped) { public ZioEntryOutputStream(int compression, OutputStream wrapped) {
this.wrapped = wrapped; this.wrapped = wrapped;
if (compression != 0) { if (compression != 0)
deflater = new Deflater(Deflater.BEST_COMPRESSION, true); downstream = new DeflaterOutputStream(wrapped, new Deflater(Deflater.BEST_COMPRESSION, true));
downstream = new DeflaterOutputStream(wrapped, deflater); else downstream = wrapped;
} else {
downstream = wrapped;
}
} }
public void close() throws IOException { public void close() throws IOException {
downstream.flush(); downstream.flush();
downstream.close(); downstream.close();
crcValue = (int) crc.getValue(); crcValue = (int) crc.getValue();
if (deflater != null) {
deflater.end();
}
} }
public int getCRC() { public int getCRC() {

View File

@ -28,7 +28,7 @@ import java.util.Locale;
*/ */
public class ZipListingHelper { public class ZipListingHelper {
static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yy HH:mm", Locale.ENGLISH); static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yy HH:mm");
public static void listHeader(LoggerInterface log) { public static void listHeader(LoggerInterface log) {
log.debug(" Length Method Size Ratio Date Time CRC-32 Name"); log.debug(" Length Method Size Ratio Date Time CRC-32 Name");

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.content.Context;
import android.util.Log; 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.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.GeneralName; 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.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 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.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -46,12 +49,6 @@ import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; 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 // TODO Address exception handling in a uniform way throughout
@SuppressWarnings("LineLength") @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.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
@ -10,18 +10,16 @@ import android.graphics.Bitmap.Config;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; 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.fdroid.fdroid.data.SanitizedFile;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
@ -42,22 +40,20 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream; 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 * 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. * 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 * 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. * and the relevant .apk and icon files available.
*/ */
@SuppressWarnings("LineLength")
public final class LocalRepoManager { public final class LocalRepoManager {
private static final String TAG = "LocalRepoManager"; private static final String TAG = "LocalRepoManager";
@ -66,13 +62,13 @@ public final class LocalRepoManager {
private final AssetManager assetManager; private final AssetManager assetManager;
private final String fdroidPackageName; private final String fdroidPackageName;
public static final String[] WEB_ROOT_ASSET_FILES = { private static final String[] WEB_ROOT_ASSET_FILES = {
"swap-icon.png", "swap-icon.png",
"swap-tick-done.png", "swap-tick-done.png",
"swap-tick-not-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 xmlIndexJar;
private final SanitizedFile xmlIndexJarUnsigned; private final SanitizedFile xmlIndexJarUnsigned;
@ -107,7 +103,7 @@ public final class LocalRepoManager {
repoDir = new SanitizedFile(fdroidDir, "repo"); repoDir = new SanitizedFile(fdroidDir, "repo");
repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO"); repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO");
iconsDir = new SanitizedFile(repoDir, "icons"); iconsDir = new SanitizedFile(repoDir, "icons");
xmlIndexJar = new SanitizedFile(repoDir, IndexUpdater.SIGNED_FILE_NAME); xmlIndexJar = new SanitizedFile(repoDir, "index.jar");
xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar"); xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar");
if (!fdroidDir.exists() && !fdroidDir.mkdir()) { if (!fdroidDir.exists() && !fdroidDir.mkdir()) {
@ -125,7 +121,7 @@ public final class LocalRepoManager {
private String writeFdroidApkToWebroot() { private String writeFdroidApkToWebroot() {
ApplicationInfo appInfo; ApplicationInfo appInfo;
String fdroidClientURL = "https://f-droid.org/F-Droid.apk"; String fdroidClientURL = "https://f-droid.org/FDroid.apk";
try { try {
appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA); appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA);
@ -150,24 +146,10 @@ public final class LocalRepoManager {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter( BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(indexHtml))); new FileOutputStream(indexHtml)));
StringBuilder builder = new StringBuilder();
for (App app : apps.values()) {
builder.append("<li><a href=\"/fdroid/repo/")
.append(app.installedApk.apkName)
.append("\"><img width=\"32\" height=\"32\" src=\"/fdroid/repo/icons/")
.append(app.packageName)
.append("_")
.append(app.installedApk.versionCode)
.append(".png\">")
.append(app.name)
.append("</a></li>\n");
}
String line; String line;
while ((line = in.readLine()) != null) { while ((line = in.readLine()) != null) {
line = line.replaceAll("\\{\\{REPO_URL\\}\\}", repoAddress); line = line.replaceAll("\\{\\{REPO_URL\\}\\}", repoAddress);
line = line.replaceAll("\\{\\{CLIENT_URL\\}\\}", fdroidClientURL); line = line.replaceAll("\\{\\{CLIENT_URL\\}\\}", fdroidClientURL);
line = line.replaceAll("\\{\\{APP_LIST\\}\\}", builder.toString());
out.write(line); out.write(line);
} }
in.close(); in.close();
@ -249,10 +231,6 @@ public final class LocalRepoManager {
return xmlIndexJar; return xmlIndexJar;
} }
public File getWebRoot() {
return webRoot;
}
public void deleteRepo() { public void deleteRepo() {
deleteContents(repoDir); deleteContents(repoDir);
} }
@ -277,10 +255,12 @@ public final class LocalRepoManager {
} }
public void addApp(Context context, String packageName) { public void addApp(Context context, String packageName) {
App app = null; App app;
try { try {
InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName); app = SwapService.getAppFromCache(packageName);
app = App.getInstance(context, pm, installedApp, packageName); if (app == null) {
app = App.getInstance(context.getApplicationContext(), pm, packageName);
}
if (app == null || !app.isValid()) { if (app == null || !app.isValid()) {
return; return;
} }
@ -349,8 +329,7 @@ public final class LocalRepoManager {
serializer = XmlPullParserFactory.newInstance().newSerializer(); serializer = XmlPullParserFactory.newInstance().newSerializer();
} }
public void build(Context context, Map<String, App> apps, OutputStream output) public void build(Context context, Map<String, App> apps, OutputStream output) throws IOException, LocalRepoKeyStore.InitException {
throws IOException, LocalRepoKeyStore.InitException {
serializer.setOutput(output, "UTF-8"); serializer.setOutput(output, "UTF-8");
serializer.startDocument(null, null); serializer.startDocument(null, null);
serializer.startTag("", "fdroid"); serializer.startTag("", "fdroid");
@ -358,14 +337,12 @@ public final class LocalRepoManager {
// <repo> block // <repo> block
serializer.startTag("", "repo"); serializer.startTag("", "repo");
serializer.attribute("", "icon", "blah.png"); serializer.attribute("", "icon", "blah.png");
serializer.attribute("", "name", Preferences.get().getLocalRepoName() serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString);
+ " on " + FDroidApp.ipAddressString);
serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
long timestamp = System.currentTimeMillis() / 1000L; long timestamp = System.currentTimeMillis() / 1000L;
serializer.attribute("", "timestamp", String.valueOf(timestamp)); serializer.attribute("", "timestamp", String.valueOf(timestamp));
serializer.attribute("", "version", "10"); serializer.attribute("", "version", "10");
tag("description", "A local FDroid repo generated from apps installed on " tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName());
+ Preferences.get().getLocalRepoName());
serializer.endTag("", "repo"); serializer.endTag("", "repo");
// <application> blocks // <application> blocks
@ -390,7 +367,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. * That accepts a number instead of string.
* *
* @see IndexXmlBuilder#tag(String, String) * @see IndexXmlBuilder#tag(String, String)
@ -400,7 +377,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. * that accepts a date instead of a string.
* *
* @see IndexXmlBuilder#tag(String, String) * @see IndexXmlBuilder#tag(String, String)
@ -418,7 +395,7 @@ public final class LocalRepoManager {
tag("lastupdated", app.lastUpdated); tag("lastupdated", app.lastUpdated);
tag("name", app.name); tag("name", app.name);
tag("summary", app.summary); tag("summary", app.summary);
tag("icon", app.iconFromApk); tag("icon", app.icon);
tag("desc", app.description); tag("desc", app.description);
tag("license", "Unknown"); tag("license", "Unknown");
tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName()); tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName());
@ -504,7 +481,7 @@ public final class LocalRepoManager {
public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException { public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException {
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned)); BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
JarOutputStream jo = new JarOutputStream(bo); JarOutputStream jo = new JarOutputStream(bo);
JarEntry je = new JarEntry(IndexUpdater.DATA_FILE_NAME); JarEntry je = new JarEntry("index.xml");
jo.putNextEntry(je); jo.putNextEntry(je);
new IndexXmlBuilder().build(context, apps, jo); new IndexXmlBuilder().build(context, apps, jo);
jo.close(); jo.close();

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.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
@ -12,31 +13,41 @@ import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.IBinder; 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.text.TextUtils;
import android.util.Log; 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.FDroidApp;
import org.fdroid.fdroid.NotificationHelper;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.nearby.peers.Peer; import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.Downloader; 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.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Collections; import java.util.Collections;
@ -44,73 +55,188 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
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;
/** /**
* Central service which manages all of the different moving parts of swap which are required * Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps. * to enable p2p swapping of apps.
*/ */
@SuppressWarnings("LineLength")
public class SwapService extends Service { public class SwapService extends Service {
private static final String TAG = "SwapService"; private static final String TAG = "SwapService";
private static final String SHARED_PREFERENCES = "swap-state"; private static final String SHARED_PREFERENCES = "swap-state";
private static final String KEY_APPS_TO_SWAP = "appsToSwap"; private static final String KEY_APPS_TO_SWAP = "appsToSwap";
private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled"; private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled";
private static final String KEY_WIFI_ENABLED = "wifiEnabled"; 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_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_WIFI_ENABLED_BEFORE_SWAP = "wifiEnabledBeforeSwap";
private static final String KEY_HOTSPOT_ACTIVATED_BEFORE_SWAP = "hotspotEnabledBeforeSwap";
@NonNull @NonNull
private final Set<String> appsToSwap = new HashSet<>(); 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 SharedPreferences swapPreferences;
private static BluetoothAdapter bluetoothAdapter; private static BluetoothAdapter bluetoothAdapter;
private static WifiManager wifiManager; private static WifiManager wifiManager;
private static Timer pollConnectedSwapRepoTimer;
public static void stop(Context context) { public static void stop(Context context) {
Intent intent = new Intent(context, SwapService.class); Intent intent = new Intent(context, SwapService.class);
context.stopService(intent); 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 @NonNull
public Set<String> getAppsToSwap() { public Set<String> getAppsToSwap() {
return appsToSwap; return appsToSwap;
} }
@NonNull public void refreshSwap() {
public Set<Peer> getActivePeers() { if (peer != null) {
return activePeers; connectTo(peer, false);
}
} }
public void connectToPeer() { public void connectToPeer() {
if (getPeer() == null) { if (getPeer() == null) {
throw new IllegalStateException("Cannot connect to peer, no peer has been selected."); throw new IllegalStateException("Cannot connect to peer, no peer has been selected.");
} }
connectTo(getPeer()); connectTo(getPeer(), getPeer().shouldPromptForSwapBack());
if (LocalHTTPDManager.isAlive() && getPeer().shouldPromptForSwapBack()) {
askServerToSwapWithUs(peerRepo);
}
} }
public void connectTo(@NonNull Peer peer) { public void connectTo(@NonNull Peer peer, boolean requestSwapBack) {
if (peer != this.peer) { if (peer != this.peer) {
Log.e(TAG, "Oops, got a different peer to swap with than initially planned."); Log.e(TAG, "Oops, got a different peer to swap with than initially planned.");
} }
peerRepo = ensureRepoExists(peer); 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()); 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) { private Repo ensureRepoExists(@NonNull Peer peer) {
// TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with // 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. // 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; 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 // Have selected a specific peer to swap with
// (Rather than showing a generic QR code to scan) // (Rather than showing a generic QR code to scan)
@ -158,14 +296,6 @@ public class SwapService extends Service {
this.peer = peer; this.peer = peer;
} }
public void addCurrentPeerToActive() {
activePeers.add(peer);
}
public void removeCurrentPeerFromActive() {
activePeers.remove(peer);
}
public boolean isConnectingWithPeer() { public boolean isConnectingWithPeer() {
return peer != null; return peer != null;
} }
@ -252,14 +382,6 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED, visible).apply(); 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() { public static boolean wasBluetoothEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, false); 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(); 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() { public static boolean wasWifiEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, false); 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(); 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) { public boolean isEnabled() {
swapPreferences.edit().putBoolean(SwapService.KEY_HOTSPOT_ACTIVATED_BEFORE_SWAP, visible).apply(); 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 static final int NOTIFICATION = 1;
private final Binder binder = new Binder(); private final Binder binder = new Binder();
private SwapType bluetoothSwap;
private WifiSwap wifiSwap;
private static final int TIMEOUT = 15 * 60 * 1000; // 15 mins private static final int TIMEOUT = 15 * 60 * 1000; // 15 mins
@ -304,7 +461,13 @@ public class SwapService extends Service {
@Nullable @Nullable
private Timer timer; 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 class Binder extends android.os.Binder {
public SwapService getService() { public SwapService getService() {
@ -312,107 +475,55 @@ public class SwapService extends Service {
} }
} }
@Override
public void onCreate() { public void onCreate() {
super.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(); bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) { if (bluetoothAdapter != null) {
SwapService.putBluetoothEnabledBeforeSwap(bluetoothAdapter.isEnabled()); 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) { if (wifiManager != null) {
SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled()); SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled());
} }
appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, ""))); appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, "")));
bluetoothSwap = BluetoothSwap.create(this);
wifiSwap = new WifiSwap(this, wifiManager);
Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener);
localBroadcastManager.registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS)); new IntentFilter(WifiStateChangeService.BROADCAST));
localBroadcastManager.registerReceiver(bluetoothPeerFound, new IntentFilter(BluetoothManager.ACTION_FOUND));
localBroadcastManager.registerReceiver(bonjourPeerFound, new IntentFilter(BonjourManager.ACTION_FOUND));
localBroadcastManager.registerReceiver(bonjourPeerRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED));
localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS));
if (getHotspotActivatedUserPreference()) { if (getBluetoothVisibleUserPreference()) {
WifiApControl wifiApControl = WifiApControl.getInstance(this); Utils.debugLog(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically.");
if (wifiApControl != null) { bluetoothSwap.startInBackground(); // TODO replace with Intent to SwapService
wifiApControl.enable(); } else {
} Utils.debugLog(TAG, "Bluetooth was NOT enabled last time user swapped, starting not visible.");
} else if (getWifiVisibleUserPreference()) {
if (wifiManager != null) {
wifiManager.setWifiEnabled(true);
}
} }
BonjourManager.start(this); if (getWifiVisibleUserPreference()) {
BonjourManager.setVisible(this, getWifiVisibleUserPreference() || getHotspotActivatedUserPreference()); 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.");
}
} }
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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
deleteAllSwapRepos(); return START_STICKY;
Intent startUiIntent = new Intent(this, SwapWorkflowActivity.class);
startUiIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(startUiIntent);
return START_NOT_STICKY;
} }
@Override @Override
@ -424,43 +535,25 @@ public class SwapService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
compositeDisposable.dispose();
Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners.");
Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener);
localBroadcastManager.unregisterReceiver(onWifiChange); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
localBroadcastManager.unregisterReceiver(bluetoothStatus);
localBroadcastManager.unregisterReceiver(bluetoothPeerFound);
localBroadcastManager.unregisterReceiver(bonjourPeerFound);
localBroadcastManager.unregisterReceiver(bonjourPeerRemoved);
if (bluetoothAdapter != null) { if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) {
unregisterReceiver(bluetoothScanModeChanged); bluetoothAdapter.disable();
} }
BluetoothManager.stop(this);
BonjourManager.stop(this);
LocalHTTPDManager.stop(this);
if (wifiManager != null && !wasWifiEnabledBeforeSwap()) { if (wifiManager != null && !wasWifiEnabledBeforeSwap()) {
wifiManager.setWifiEnabled(false); wifiManager.setWifiEnabled(false);
} }
WifiApControl ap = WifiApControl.getInstance(this); //TODO getBluetoothSwap().stopInBackground();
if (ap != null) { getWifiSwap().stopInBackground();
if (wasHotspotEnabledBeforeSwap()) {
ap.enable();
} else {
ap.disable();
}
}
stopPollingConnectedSwapRepo();
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
} }
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); stopForeground(true);
deleteAllSwapRepos(); deleteAllSwapRepos();
@ -471,10 +564,10 @@ public class SwapService extends Service {
Intent intent = new Intent(this, SwapWorkflowActivity.class); Intent intent = new Intent(this, SwapWorkflowActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 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)) .setContentTitle(getText(R.string.local_repo_running))
.setContentText(getText(R.string.touch_to_configure_local_repo)) .setContentText(getText(R.string.touch_to_configure_local_repo))
.setSmallIcon(R.drawable.ic_nearby) .setSmallIcon(R.drawable.ic_swap)
.setContentIntent(contentIntent) .setContentIntent(contentIntent)
.build(); .build();
} }
@ -497,151 +590,40 @@ public class SwapService extends Service {
} }
} }
private void startPollingConnectedSwapRepo() { private void initTimer() {
stopPollingConnectedSwapRepo(); // TODO replace by Android scheduler
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() {
if (timer != null) { if (timer != null) {
Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset."); Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset.");
timer.cancel(); timer.cancel();
} }
Utils.debugLog(TAG, "Initializing swap timeout to " + TIMEOUT + "ms minutes"); Utils.debugLog(TAG, "Initializing swap timeout to " + TIMEOUT + "ms minutes");
timer = new Timer(TAG, true); timer = new Timer();
timer.schedule(new TimerTask() { timer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
Utils.debugLog(TAG, "Disabling swap because " + TIMEOUT + "ms passed."); 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); stop(SwapService.this);
} }
}, TIMEOUT); }, TIMEOUT);
} }
private void restartWiFiServices() { @SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
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);
}
}
private final Preferences.ChangeListener httpsEnabledListener = new Preferences.ChangeListener() { private final Preferences.ChangeListener httpsEnabledListener = new Preferences.ChangeListener() {
@Override @Override
public void onPreferenceChange() { 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() { private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent i) { 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

@ -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.bluetooth.BluetoothDevice;
import android.os.Parcel; import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
import androidx.annotation.Nullable;
public class BluetoothPeer implements Peer { public class BluetoothPeer implements Peer {
private static final String BLUETOOTH_NAME_TAG = "FDroid:";
private final BluetoothDevice device; private final BluetoothDevice device;
/** public BluetoothPeer(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) {
this.device = device; this.device = device;
} }
@ -41,18 +21,19 @@ public class BluetoothPeer implements Peer {
@Override @Override
public String getName() { public String getName() {
return device.getName().replaceAll("^" + BLUETOOTH_NAME_TAG, ""); return device.getName().replaceAll("^" + BluetoothSwap.BLUETOOTH_NAME_TAG, "");
} }
@Override @Override
public int getIcon() { public int getIcon() {
return R.drawable.ic_bluetooth; return R.drawable.ic_bluetooth_white;
} }
@Override @Override
public boolean equals(Object peer) { public boolean equals(Object peer) {
return peer instanceof BluetoothPeer return peer != null
&& TextUtils.equals(((BluetoothPeer) peer).device.getAddress(), device.getAddress()); && peer instanceof BluetoothPeer
&& ((BluetoothPeer) peer).device.getAddress().equals(device.getAddress());
} }
@Override @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. * 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 * 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 * 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. * trust it enough to receive an index from it that contains a fingerprint we can use.

View File

@ -0,0 +1,165 @@
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 java.io.IOException;
import java.net.InetAddress;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;
@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.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.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
import javax.jmdns.ServiceInfo; import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.FDroidServiceInfo; import javax.jmdns.impl.FDroidServiceInfo;
import androidx.annotation.Nullable;
public class BonjourPeer extends WifiPeer { 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; private final FDroidServiceInfo serviceInfo;
/** public BonjourPeer(ServiceInfo 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) {
this.serviceInfo = new FDroidServiceInfo(serviceInfo); this.serviceInfo = new FDroidServiceInfo(serviceInfo);
this.name = serviceInfo.getDomain(); this.name = serviceInfo.getDomain();
this.uri = Uri.parse(this.serviceInfo.getRepoAddress()); this.uri = Uri.parse(this.serviceInfo.getRepoAddress());
@ -54,12 +28,17 @@ public class BonjourPeer extends WifiPeer {
} }
@Override @Override
public int hashCode() { public boolean equals(Object peer) {
String fingerprint = getFingerprint(); if (peer != null && peer instanceof BonjourPeer) {
if (fingerprint == null) { BonjourPeer that = (BonjourPeer) peer;
return 0; return this.getFingerprint().equals(that.getFingerprint());
} }
return fingerprint.hashCode(); return false;
}
@Override
public int hashCode() {
return getFingerprint().hashCode();
} }
@Override @Override

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.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.text.TextUtils;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.NewRepoConfig;
@ -27,35 +26,6 @@ public class WifiPeer implements Peer {
this.shouldPromptForSwapBack = shouldPromptForSwapBack; 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 @Override
public String getName() { public String getName() {
return name; return name;
@ -63,7 +33,7 @@ public class WifiPeer implements Peer {
@Override @Override
public int getIcon() { public int getIcon() {
return R.drawable.ic_wifi; return R.drawable.ic_network_wifi_white;
} }
@Override @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,505 +0,0 @@
package org.fdroid.fdroid.nearby;
/*
* #%L
* NanoHttpd-Webserver
* %%
* Copyright (C) 2012 - 2015 nanohttpd
* %%
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the nanohttpd nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
import android.content.Context;
import android.net.Uri;
import org.fdroid.fdroid.BuildConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
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;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
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
* plugin interface, and custom CORS header manipulation.
* <p>
* This is mostly just synced from {@code SimpleWebServer.java} from NanoHTTPD.
*
* @see <a href="https://github.com/NanoHttpd/nanohttpd/blob/nanohttpd-project-2.3.1/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java">webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java</a>
*/
public class LocalHTTPD extends NanoHTTPD {
private static final String TAG = "LocalHTTPD";
/**
* Default Index file names.
*/
public static final String[] INDEX_FILE_NAMES = {"index.html"};
private final WeakReference<Context> context;
protected List<File> rootDirs;
// Date format specified by RFC 7231 section 7.1.1.1.
private static final DateFormat RFC_1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
static {
RFC_1123.setLenient(false);
RFC_1123.setTimeZone(TimeZone.getTimeZone("GMT"));
}
/**
* Configure and start the webserver. This also sets the MIME Types only
* for files that should be downloadable when a browser is used to display
* the swap repo, rather than the F-Droid client. The other file types
* should not be added because it could expose exploits to the browser.
*/
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());
if (useHttps) {
enableHTTPS();
}
MIME_TYPES = new HashMap<>(); // ignore nanohttpd's list
MIME_TYPES.put("apk", "application/vnd.android.package-archive");
MIME_TYPES.put("html", "text/html");
MIME_TYPES.put("png", "image/png");
MIME_TYPES.put("xml", "application/xml");
}
private boolean canServeUri(String uri, File homeDir) {
boolean canServeUri;
File f = new File(homeDir, uri);
canServeUri = f.exists();
return canServeUri;
}
/**
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
* instead of '+'.
*/
private String encodeUri(String uri) {
String newUri = "";
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
if ("/".equals(tok)) {
newUri += "/";
} else if (" ".equals(tok)) {
newUri += "%20";
} else {
try {
newUri += URLEncoder.encode(tok, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
}
}
}
return newUri;
}
private String findIndexFileInDirectory(File directory) {
for (String fileName : LocalHTTPD.INDEX_FILE_NAMES) {
File indexFile = new File(directory, fileName);
if (indexFile.isFile()) {
return fileName;
}
}
return null;
}
protected Response getForbiddenResponse(String s) {
return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
}
protected Response getInternalErrorResponse(String s) {
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
}
protected Response getNotFoundResponse() {
return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
}
protected String listDirectory(String uri, File f) {
String heading = "Directory " + uri;
StringBuilder msg =
new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
+ "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
String up = null;
if (uri.length() > 1) {
String u = uri.substring(0, uri.length() - 1);
int slash = u.lastIndexOf('/');
if (slash >= 0 && slash < u.length()) {
up = uri.substring(0, slash + 1);
}
}
List<String> files = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isFile();
}
}));
Collections.sort(files);
List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isDirectory();
}
}));
Collections.sort(directories);
if (up != null || directories.size() + files.size() > 0) {
msg.append("<ul>");
if (up != null || directories.size() > 0) {
msg.append("<section class=\"directories\">");
if (up != null) {
msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></li>");
}
for (String directory : directories) {
String dir = directory + "/";
msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir).append("</span></a></li>");
}
msg.append("</section>");
}
if (files.size() > 0) {
msg.append("<section class=\"files\">");
for (String file : files) {
msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
File curFile = new File(f, file);
long len = curFile.length();
msg.append("&nbsp;<span class=\"filesize\">(");
if (len < 1024) {
msg.append(len).append(" bytes");
} else if (len < 1024 * 1024) {
msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
} else {
msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
}
msg.append(")</span></li>");
}
msg.append("</section>");
}
msg.append("</ul>");
}
msg.append("</body></html>");
return msg.toString();
}
/**
* {@link Response#setKeepAlive(boolean)} alone does not seem to stop
* setting the {@code Connection} header to {@code keep-alive}, so also
* just directly set that header.
*/
public static Response addResponseHeaders(Response response) {
response.setKeepAlive(false);
response.setGzipEncoding(false);
response.addHeader("Connection", "close");
response.addHeader("Content-Security-Policy",
"default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline';");
return response;
}
public static Response newFixedLengthResponse(String msg) {
return addResponseHeaders(NanoHTTPD.newFixedLengthResponse(msg));
}
public static Response newFixedLengthResponse(Response.IStatus status, String mimeType,
InputStream data, long totalBytes) {
return addResponseHeaders(NanoHTTPD.newFixedLengthResponse(status, mimeType, data, totalBytes));
}
public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) {
Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message);
addResponseHeaders(response);
response.addHeader("Accept-Ranges", "bytes");
return response;
}
private Response respond(Map<String, String> headers, IHTTPSession session, String uri) {
return defaultRespond(headers, session, uri);
}
private Response defaultRespond(Map<String, String> headers, IHTTPSession session, String uri) {
// Remove URL arguments
uri = uri.trim().replace(File.separatorChar, '/');
if (uri.indexOf('?') >= 0) {
uri = uri.substring(0, uri.indexOf('?'));
}
// Prohibit getting out of current directory
if (uri.contains("../")) {
return getForbiddenResponse("Won't serve ../ for security reasons.");
}
boolean canServeUri = false;
File homeDir = null;
for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) {
homeDir = this.rootDirs.get(i);
canServeUri = canServeUri(uri, homeDir);
}
if (!canServeUri) {
return getNotFoundResponse();
}
// Browsers get confused without '/' after the directory, send a
// redirect.
File f = new File(homeDir, uri);
if (f.isDirectory() && !uri.endsWith("/")) {
uri += "/";
Response res =
newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, "<html><body>Redirected: <a href=\"" + uri + "\">" + uri + "</a></body></html>");
res.addHeader("Location", uri);
return res;
}
if (f.isDirectory()) {
// First look for index files (index.html, index.htm, etc) and if
// none found, list the directory if readable.
String indexFile = findIndexFileInDirectory(f);
if (indexFile == null) {
if (f.canRead()) {
// No index file, list the directory if it is readable
return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f));
} else {
return getForbiddenResponse("No directory listing.");
}
} else {
return respond(headers, session, uri + indexFile);
}
}
String mimeTypeForFile = getMimeTypeForFile(uri);
Response response = serveFile(uri, headers, f, mimeTypeForFile);
return response != null ? response : getNotFoundResponse();
}
@Override
public Response serve(IHTTPSession session) {
Map<String, String> header = session.getHeaders();
Map<String, String> parms = session.getParms();
String uri = session.getUri();
if (BuildConfig.DEBUG) {
System.out.println(session.getMethod() + " '" + uri + "' ");
Iterator<String> e = header.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'");
}
e = parms.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'");
}
}
if (session.getMethod() == Method.POST) {
try {
session.parseBody(new HashMap<String, String>());
} catch (IOException e) {
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
"Internal server error, check logcat on server for details.");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
}
return handlePost(session);
}
for (File homeDir : this.rootDirs) {
// Make sure we won't die of an exception later
if (!homeDir.isDirectory()) {
return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
}
}
return respond(Collections.unmodifiableMap(header), session, uri);
}
private Response handlePost(IHTTPSession session) {
Uri uri = Uri.parse(session.getUri());
switch (uri.getPath()) {
case "/request-swap":
if (!session.getParms().containsKey("repo")) {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
"Requires 'repo' parameter to be posted.");
}
SwapWorkflowActivity.requestSwap(context.get(), session.getParms().get("repo"));
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
}
return newFixedLengthResponse("");
}
/**
* Serves file from homeDir and its' subdirectories (only). Uses only URI,
* ignores all headers and HTTP parameters.
*/
Response serveFile(String uri, Map<String, String> header, File file, String mime) {
Response res;
try {
// Calculate etag
String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode());
// Support (simple) skipping:
long startFrom = 0;
long endAt = -1;
String range = header.get("range");
if (range != null) {
if (range.startsWith("bytes=")) {
range = range.substring("bytes=".length());
int minus = range.indexOf('-');
try {
if (minus > 0) {
startFrom = Long.parseLong(range.substring(0, minus));
endAt = Long.parseLong(range.substring(minus + 1));
}
} catch (NumberFormatException ignored) {
}
}
}
// get if-range header. If present, it must match etag or else we
// should ignore the range request
String ifRange = header.get("if-range");
boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));
String ifNoneMatch = header.get("if-none-match");
boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag));
// Change return code and add Content-Range header when skipping is
// requested
long fileLen = file.length();
if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
// range request that matches current etag
// and the startFrom of the range is satisfiable
if (headerIfNoneMatchPresentAndMatching) {
// range request that matches current etag
// and the startFrom of the range is satisfiable
// would return range from file
// respond with not-modified
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
res.addHeader("ETag", etag);
} else {
if (endAt < 0) {
endAt = fileLen - 1;
}
long newLen = endAt - startFrom + 1;
if (newLen < 0) {
newLen = 0;
}
FileInputStream fis = new FileInputStream(file);
fis.skip(startFrom);
res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen);
res.addHeader("Accept-Ranges", "bytes");
res.addHeader("Content-Length", "" + newLen);
res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
res.addHeader("ETag", etag);
res.addHeader("Last-Modified", RFC_1123.format(new Date(file.lastModified())));
}
} else {
if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
// return the size of the file
// 4xx responses are not trumped by if-none-match
res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
res.addHeader("Content-Range", "bytes */" + fileLen);
res.addHeader("ETag", etag);
} else if (range == null && headerIfNoneMatchPresentAndMatching) {
// full-file-fetch request
// would return entire file
// respond with not-modified
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
res.addHeader("ETag", etag);
} else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
// range request that doesn't match current etag
// would return entire (different) file
// respond with not-modified
res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
res.addHeader("ETag", etag);
} else {
// supply the file
res = newFixedFileResponse(file, mime);
res.addHeader("Content-Length", "" + fileLen);
res.addHeader("ETag", etag);
res.addHeader("Last-Modified", RFC_1123.format(new Date(file.lastModified())));
}
}
} catch (IOException ioe) {
res = getForbiddenResponse("Reading file failed.");
}
return addResponseHeaders(res);
}
private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException {
Response res;
res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length());
addResponseHeaders(res);
res.addHeader("Accept-Ranges", "bytes");
return res;
}
private void enableHTTPS() {
try {
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context.get());
SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
localRepoKeyStore.getKeyStore(),
localRepoKeyStore.getKeyManagers());
makeSecure(factory, null);
} catch (LocalRepoKeyStore.InitException | IOException e) {
e.printStackTrace();
}
}
}

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,175 +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.Manifest;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Process;
import android.util.Log;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.Utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
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
* 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
* for removeable storage devices plugged in via USB, since do not show up as
* "External Storage"
* <p>
* Scanning the removable storage requires that the user allowed it. This
* requires both the {@link org.fdroid.fdroid.Preferences#isScanRemovableStorageEnabled()}
* and the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}
* permission to be enabled.
*
* @see TreeUriScannerIntentService TreeUri method for writing repos to be shared
* @see <a href="https://stackoverflow.com/a/40201333">Universal way to write to external SD card on Android</a>
* @see <a href="https://commonsware.com/blog/2017/11/14/storage-situation-external-storage.html"> The Storage Situation: External Storage </a>
*/
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 List<String> SKIP_DIRS = Arrays.asList(".android_secure", "LOST.DIR");
public SDCardScannerService() {
super("SDCardScannerService");
}
public static void scan(Context context) {
Intent intent = new Intent(context, SDCardScannerService.class);
intent.setAction(ACTION_SCAN);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null || !ACTION_SCAN.equals(intent.getAction())) {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
HashSet<File> files = new HashSet<>();
if (Build.VERSION.SDK_INT < 21) {
if (Environment.isExternalStorageRemovable()) {
File sdcard = Environment.getExternalStorageDirectory();
String state = Environment.getExternalStorageState();
Collections.addAll(files, checkExternalStorage(sdcard, state));
}
} else {
for (File f : getExternalFilesDirs(null)) {
Log.i(TAG, "getExternalFilesDirs " + f);
if (f == null || !f.isDirectory()) {
continue;
}
Log.i(TAG, "getExternalFilesDirs " + f);
if (Environment.isExternalStorageRemovable(f)) {
String state = Environment.getExternalStorageState(f);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
// remove Android/data/org.fdroid.fdroid/files to get root
File sdcard = f.getParentFile().getParentFile().getParentFile().getParentFile();
Collections.addAll(files, checkExternalStorage(sdcard, state));
} else {
Collections.addAll(files, checkExternalStorage(f, state));
}
}
}
}
Log.i(TAG, "sdcard files " + files.toString());
ArrayList<String> filesList = new ArrayList<>();
for (File dir : files) {
if (!dir.isDirectory()) {
continue;
}
searchDirectory(dir);
}
}
private File[] checkExternalStorage(File sdcard, String state) {
File[] files = null;
if (sdcard != null &&
(Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) || Environment.MEDIA_MOUNTED.equals(state))) {
files = sdcard.listFiles();
}
if (files == null) {
Utils.debugLog(TAG, "checkExternalStorage returned blank, F-Droid probaby doesn't have Storage perm!");
return new File[0];
} else {
return files;
}
}
private void searchDirectory(File dir) {
if (SKIP_DIRS.contains(dir.getName())) {
return;
}
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
searchDirectory(file);
} else {
if (IndexV1Updater.SIGNED_FILE_NAME.equals(file.getName())) {
registerRepo(file);
}
}
}
}
private void registerRepo(File file) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
TreeUriScannerIntentService.registerRepo(this, inputStream, Uri.fromFile(file.getParentFile()));
} catch (IOException | IndexUpdater.SigningException e) {
e.printStackTrace();
} finally {
Utils.closeQuietly(inputStream);
}
}
}

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,210 +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.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.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.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
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
* Storage Access Framework. Permission must first be granted by the user
* {@link android.content.Intent#ACTION_OPEN_DOCUMENT_TREE} or
* {@link android.os.storage.StorageVolume#createAccessIntent(String)}request,
* then F-Droid will have permanent access to that{@link Uri}.
* <p>
* Even though the Storage Access Framework was introduced in
* {@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://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>
*/
@TargetApi(21)
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";
public TreeUriScannerIntentService() {
super("TreeUriScannerIntentService");
}
public static void scan(Context context, Uri data) {
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
protected void onHandleIntent(Intent intent) {
if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) {
return;
}
Uri treeUri = intent.getData();
if (treeUri == null) {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
DocumentFile treeFile = DocumentFile.fromTreeUri(this, treeUri);
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) {
if (IndexV1Updater.SIGNED_FILE_NAME.equals(documentFile.getName())) {
registerRepo(documentFile);
foundIndex = true;
}
}
}
for (DocumentFile dir : dirs) {
searchDirectory(dir);
}
}
/**
* For all files called {@link IndexV1Updater#SIGNED_FILE_NAME} found, check
* the JAR signature and read the fingerprint of the signing certificate.
* The fingerprint is then used to find whether this local repo is a mirror
* of an existing repo, or a totally new repo. In order to verify the
* signatures in the JAR, the whole file needs to be read in first.
*
* @see JarInputStream#JarInputStream(InputStream, boolean)
*/
private void registerRepo(DocumentFile index) {
InputStream inputStream = null;
try {
inputStream = getContentResolver().openInputStream(index.getUri());
registerRepo(this, inputStream, index.getParentFile().getUri());
} catch (IOException | IndexUpdater.SigningException e) {
e.printStackTrace();
} finally {
Utils.closeQuietly(inputStream);
}
}
public static void registerRepo(Context context, InputStream inputStream, Uri repoUri)
throws IOException, IndexUpdater.SigningException {
if (inputStream == null) {
return;
}
File destFile = File.createTempFile("dl-", IndexV1Updater.SIGNED_FILE_NAME, context.getCacheDir());
FileUtils.copyInputStreamToFile(inputStream, destFile);
JarFile jarFile = new JarFile(destFile, true);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexV1Updater.DATA_FILE_NAME);
IOUtils.readLines(jarFile.getInputStream(indexEntry));
Certificate certificate = IndexUpdater.getSigningCertFromJar(indexEntry);
String fingerprint = Utils.calcFingerprint(certificate);
Log.i(TAG, "Got fingerprint: " + fingerprint);
destFile.delete();
Log.i(TAG, "Found a valid, signed index-v1.json");
for (Repo repo : RepoProvider.Helper.all(context)) {
if (fingerprint.equals(repo.fingerprint)) {
Log.i(TAG, repo.address + " has the SAME fingerprint: " + fingerprint);
} else {
Log.i(TAG, repo.address + " different fingerprint");
}
}
AddRepoIntentService.addRepo(context, repoUri, fingerprint);
// TODO rework IndexUpdater.getSigningCertFromJar to work for here
}
}

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

@ -0,0 +1,406 @@
package org.fdroid.fdroid.net;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.webkit.MimeTypeMap;
import fi.iki.elonen.NanoHTTPD;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Utils;
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.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class LocalHTTPD extends NanoHTTPD {
private static final String TAG = "LocalHTTPD";
private final Context context;
private final File webRoot;
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
super(hostname, port);
this.webRoot = webRoot;
this.context = context.getApplicationContext();
if (useHttps) {
enableHTTPS();
}
}
/**
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
* instead of '+'.
*/
private String encodeUriBetweenSlashes(String uri) {
String newUri = "";
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
switch (tok) {
case "/":
newUri += "/";
break;
case " ":
newUri += "%20";
break;
default:
try {
newUri += URLEncoder.encode(tok, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
}
break;
}
}
return newUri;
}
private void requestSwap(String repo) {
Utils.debugLog(TAG, "Received request to swap with " + repo);
Utils.debugLog(TAG, "Showing confirm screen to check whether that is okay with the user.");
Uri repoUri = Uri.parse(repo);
Intent intent = new Intent(context, SwapWorkflowActivity.class);
intent.setData(repoUri);
intent.putExtra(SwapWorkflowActivity.EXTRA_CONFIRM, true);
intent.putExtra(SwapWorkflowActivity.EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@Override
public Response serve(IHTTPSession session) {
if (session.getMethod() == Method.POST) {
try {
session.parseBody(new HashMap<String, String>());
} catch (IOException e) {
Log.e(TAG, "An error occured while parsing the POST body", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
"Internal server error, check logcat on server for details.");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
}
return handlePost(session);
}
return handleGet(session);
}
private Response handlePost(IHTTPSession session) {
Uri uri = Uri.parse(session.getUri());
switch (uri.getPath()) {
case "/request-swap":
if (!session.getParms().containsKey("repo")) {
Log.e(TAG, "Malformed /request-swap request to local repo HTTP server."
+ " Should have posted a 'repo' parameter.");
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
"Requires 'repo' parameter to be posted.");
}
requestSwap(session.getParms().get("repo"));
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
}
return newFixedLengthResponse("");
}
private Response handleGet(IHTTPSession session) {
Map<String, String> header = session.getHeaders();
Map<String, String> parms = session.getParms();
String uri = session.getUri();
if (BuildConfig.DEBUG) {
Utils.debugLog(TAG, session.getMethod() + " '" + uri + "' ");
Iterator<String> e = header.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
Utils.debugLog(TAG, " HDR: '" + value + "' = '" + header.get(value) + "'");
}
e = parms.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
Utils.debugLog(TAG, " PRM: '" + value + "' = '" + parms.get(value) + "'");
}
}
if (!webRoot.isDirectory()) {
return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
"INTERNAL ERRROR: given path is not a directory (" + webRoot + ").");
}
return respond(Collections.unmodifiableMap(header), uri);
}
private void enableHTTPS() {
try {
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
localRepoKeyStore.getKeyStore(),
localRepoKeyStore.getKeyManagers());
makeSecure(factory, null);
} catch (LocalRepoKeyStore.InitException | IOException e) {
Log.e(TAG, "Could not enable HTTPS", e);
}
}
private Response respond(Map<String, String> headers, String uri) {
// Remove URL arguments
uri = uri.trim().replace(File.separatorChar, '/');
if (uri.indexOf('?') >= 0) {
uri = uri.substring(0, uri.indexOf('?'));
}
// Prohibit getting out of current directory
if (uri.contains("../")) {
return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: Won't serve ../ for security reasons.");
}
File f = new File(webRoot, uri);
if (!f.exists()) {
return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
"Error 404, file not found.");
}
// Browsers get confused without '/' after the directory, send a
// redirect.
if (f.isDirectory() && !uri.endsWith("/")) {
uri += "/";
Response res = createResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML,
"<html><body>Redirected: <a href=\"" +
uri + "\">" + uri + "</a></body></html>");
res.addHeader("Location", uri);
return res;
}
if (f.isDirectory()) {
// First look for index files (index.html, index.htm, etc) and if
// none found, list the directory if readable.
String indexFile = findIndexFileInDirectory(f);
if (indexFile == null) {
if (f.canRead()) {
// No index file, list the directory if it is readable
return createResponse(Response.Status.OK, NanoHTTPD.MIME_HTML,
listDirectory(uri, f));
} else {
return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: No directory listing.");
}
} else {
return respond(headers, uri + indexFile);
}
}
Response response = serveFile(headers, f, getAndroidMimeTypeForFile(uri));
return response != null ? response :
createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
"Error 404, file not found.");
}
/**
* Serves file from homeDir and its' subdirectories (only). Uses only URI,
* ignores all headers and HTTP parameters.
*/
private Response serveFile(Map<String, String> header, File file, String mime) {
Response res;
try {
// Calculate etag
String etag = Integer
.toHexString((file.getAbsolutePath() + file.lastModified() + String.valueOf(file.length()))
.hashCode());
// Support (simple) skipping:
long startFrom = 0;
long endAt = -1;
String range = header.get("range");
if (range != null && range.startsWith("bytes=")) {
range = range.substring("bytes=".length());
int minus = range.indexOf('-');
try {
if (minus > 0) {
startFrom = Long.parseLong(range.substring(0, minus));
endAt = Long.parseLong(range.substring(minus + 1));
}
} catch (NumberFormatException ignored) {
}
}
// Change return code and add Content-Range header when skipping is
// requested
long fileLen = file.length();
if (range != null && startFrom >= 0) {
if (startFrom >= fileLen) {
res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE,
NanoHTTPD.MIME_PLAINTEXT, "");
res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
res.addHeader("ETag", etag);
} else {
if (endAt < 0) {
endAt = fileLen - 1;
}
long newLen = endAt - startFrom + 1;
if (newLen < 0) {
newLen = 0;
}
final long dataLen = newLen;
FileInputStream fis = new FileInputStream(file) {
@Override
public int available() throws IOException {
return (int) dataLen;
}
};
long skipped = fis.skip(startFrom);
if (skipped != startFrom) {
throw new IOException("unable to skip the required " + startFrom + " bytes.");
}
res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis);
res.addHeader("Content-Length", String.valueOf(dataLen));
res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
+ fileLen);
res.addHeader("ETag", etag);
}
} else {
if (etag.equals(header.get("if-none-match"))) {
res = createResponse(Response.Status.NOT_MODIFIED, mime, "");
} else {
res = createResponse(Response.Status.OK, mime, new FileInputStream(file));
res.addHeader("Content-Length", String.valueOf(fileLen));
res.addHeader("ETag", etag);
}
}
} catch (IOException ioe) {
res = createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: Reading file failed.");
}
return res;
}
// Announce that the file server accepts partial content requests
private Response createResponse(Response.Status status, String mimeType, InputStream message) {
Response res = newChunkedResponse(status, mimeType, message);
res.addHeader("Accept-Ranges", "bytes");
return res;
}
// Announce that the file server accepts partial content requests
private Response createResponse(Response.Status status, String mimeType, String message) {
Response res = newFixedLengthResponse(status, mimeType, message);
res.addHeader("Accept-Ranges", "bytes");
return res;
}
private static String getAndroidMimeTypeForFile(String uri) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
if (extension != null) {
MimeTypeMap mime = MimeTypeMap.getSingleton();
type = mime.getMimeTypeFromExtension(extension);
}
return type;
}
private String findIndexFileInDirectory(File directory) {
String indexFileName = "index.html";
File indexFile = new File(directory, indexFileName);
if (indexFile.exists()) {
return indexFileName;
}
return null;
}
private String listDirectory(String uri, File f) {
String heading = "Directory " + uri;
StringBuilder msg = new StringBuilder("<html><head><title>" + heading
+ "</title><style><!--\n" +
"span.dirname { font-weight: bold; }\n" +
"span.filesize { font-size: 75%; }\n" +
"// -->\n" +
"</style>" +
"</head><body><h1>" + heading + "</h1>");
String up = null;
if (uri.length() > 1) {
String u = uri.substring(0, uri.length() - 1);
int slash = u.lastIndexOf('/');
if (slash >= 0 && slash < u.length()) {
up = uri.substring(0, slash + 1);
}
}
List<String> files = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isFile();
}
}));
Collections.sort(files);
List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isDirectory();
}
}));
Collections.sort(directories);
if (up != null || directories.size() + files.size() > 0) {
msg.append("<ul>");
if (up != null || directories.size() > 0) {
msg.append("<section class=\"directories\">");
if (up != null) {
msg.append("<li><a rel=\"directory\" href=\"").append(up)
.append("\"><span class=\"dirname\">..</span></a></b></li>");
}
for (String directory : directories) {
String dir = directory + "/";
msg.append("<li><a rel=\"directory\" href=\"")
.append(encodeUriBetweenSlashes(uri + dir))
.append("\"><span class=\"dirname\">").append(dir)
.append("</span></a></b></li>");
}
msg.append("</section>");
}
if (files.size() > 0) {
msg.append("<section class=\"files\">");
for (String file : files) {
msg.append("<li><a href=\"").append(encodeUriBetweenSlashes(uri + file))
.append("\"><span class=\"filename\">").append(file)
.append("</span></a>");
File curFile = new File(f, file);
long len = curFile.length();
msg.append("&nbsp;<span class=\"filesize\">(");
if (len < 1024) {
msg.append(len).append(" bytes");
} else if (len < 1024 * 1024) {
msg.append(len / 1024).append('.').append(len % 1024 / 10 % 100)
.append(" KB");
} else {
msg.append(len / (1024 * 1024)).append('.')
.append(len % (1024 * 1024) / 10 % 100).append(" MB");
}
msg.append(")</span></li>");
}
msg.append("</section>");
}
msg.append("</ul>");
}
msg.append("</body></html>");
return msg.toString();
}
}

View File

@ -1,4 +1,4 @@
package org.fdroid.fdroid.nearby; package org.fdroid.fdroid.net;
import android.app.IntentService; import android.app.IntentService;
import android.content.ComponentName; import android.content.ComponentName;
@ -6,25 +6,21 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.DhcpInfo; import android.net.DhcpInfo;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Build; import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; 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.apache.commons.net.util.SubnetUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo; 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.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
@ -35,14 +31,11 @@ import java.security.cert.Certificate;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Locale; 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. * Handle state changes to the device's wifi, storing the required bits.
* The {@link Intent} that starts it either has no extras included, * 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 * 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. * which case an instance of {@link NetworkInfo} is included.
* <p> * <p>
* The work is done in a {@link Thread} so that new incoming {@code Intents} * The work is done in a {@link Thread} so that new incoming {@code Intents}
@ -65,14 +58,10 @@ public class WifiStateChangeService extends IntentService {
private static final String TAG = "WifiStateChangeService"; private static final String TAG = "WifiStateChangeService";
public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE"; public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
public static final String EXTRA_STATUS = "wifiStateChangeStatus";
private WifiManager wifiManager; private WifiManager wifiManager;
private static WifiInfoThread wifiInfoThread; private static WifiInfoThread wifiInfoThread;
private static int previousWifiState = Integer.MIN_VALUE; private static int previousWifiState = Integer.MIN_VALUE;
private static int wifiState;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public WifiStateChangeService() { public WifiStateChangeService() {
super("WifiStateChangeService"); super("WifiStateChangeService");
@ -86,12 +75,6 @@ public class WifiStateChangeService extends IntentService {
context.startService(intent); context.startService(intent);
} }
@Override
public void onDestroy() {
compositeDisposable.dispose();
super.onDestroy();
}
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
@ -101,11 +84,10 @@ public class WifiStateChangeService extends IntentService {
} }
Utils.debugLog(TAG, "WiFi change service started."); Utils.debugLog(TAG, "WiFi change service started.");
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class); wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
wifiState = wifiManager.getWifiState(); int wifiState = wifiManager.getWifiState();
Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState)); if (ni == null || ni.isConnected()) {
if (ni == null Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState));
|| ni.getState() == NetworkInfo.State.CONNECTED || ni.getState() == NetworkInfo.State.DISCONNECTED) {
if (previousWifiState != wifiState && if (previousWifiState != wifiState &&
(wifiState == WifiManager.WIFI_STATE_ENABLED (wifiState == WifiManager.WIFI_STATE_ENABLED
|| wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot
@ -119,12 +101,13 @@ public class WifiStateChangeService extends IntentService {
} }
if (Build.VERSION.SDK_INT < 21 && wifiState == WifiManager.WIFI_STATE_ENABLED) { 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 { public class WifiInfoThread extends Thread {
private static final String TAG = "WifiInfoThread";
@Override @Override
public void run() { public void run() {
@ -143,7 +126,6 @@ public class WifiStateChangeService extends IntentService {
if (wifiState == WifiManager.WIFI_STATE_ENABLED) { if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
wifiInfo = wifiManager.getConnectionInfo(); wifiInfo = wifiManager.getConnectionInfo();
FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress());
setSsid(wifiInfo);
DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
if (dhcpInfo != null) { if (dhcpInfo != null) {
String netmask = formatIpAddress(dhcpInfo.netmask); String netmask = formatIpAddress(dhcpInfo.netmask);
@ -161,13 +143,14 @@ public class WifiStateChangeService extends IntentService {
setIpInfoFromNetworkInterface(); setIpInfoFromNetworkInterface();
} }
} else if (wifiState == WifiManager.WIFI_STATE_DISABLED } else if (wifiState == WifiManager.WIFI_STATE_DISABLED
|| wifiState == WifiManager.WIFI_STATE_DISABLING || wifiState == WifiManager.WIFI_STATE_DISABLING) {
|| wifiState == WifiManager.WIFI_STATE_UNKNOWN) {
// try once to see if its a hotspot // try once to see if its a hotspot
setIpInfoFromNetworkInterface(); setIpInfoFromNetworkInterface();
if (FDroidApp.ipAddressString == null) { if (FDroidApp.ipAddressString == null) {
return; return;
} }
} else { // a hotspot can be active during WIFI_STATE_UNKNOWN
setIpInfoFromNetworkInterface();
} }
if (retryCount > 120) { if (retryCount > 120) {
@ -184,7 +167,17 @@ public class WifiStateChangeService extends IntentService {
return; 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; String scheme;
if (Preferences.get().isLocalRepoHttpsEnabled()) { if (Preferences.get().isLocalRepoHttpsEnabled()) {
@ -234,56 +227,10 @@ public class WifiStateChangeService extends IntentService {
return; return;
} }
Intent intent = new Intent(BROADCAST); Intent intent = new Intent(BROADCAST);
intent.putExtra(EXTRA_STATUS, wifiState);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent); 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 * Search for known Wi-Fi, Hotspot, and local network interfaces and get
* the IP Address info from it. This is necessary because network * the IP Address info from it. This is necessary because network
@ -303,7 +250,7 @@ public class WifiStateChangeService extends IntentService {
while (networkInterfaces.hasMoreElements()) { while (networkInterfaces.hasMoreElements()) {
NetworkInterface netIf = networkInterfaces.nextElement(); NetworkInterface netIf = networkInterfaces.nextElement();
for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements(); ) { for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements();) {
InetAddress inetAddress = inetAddresses.nextElement(); InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) { if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) {
continue; continue;
@ -319,17 +266,11 @@ public class WifiStateChangeService extends IntentService {
// java.lang.IllegalArgumentException: Value [64] not in range [0,32] // java.lang.IllegalArgumentException: Value [64] not in range [0,32]
continue; continue;
} }
try { if (inetAddress.equals(address.getAddress()) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) {
String cidr = String.format(Locale.ENGLISH, "%s/%d", String cidr = String.format(Locale.ENGLISH, "%s/%d",
FDroidApp.ipAddressString, networkPrefixLength); FDroidApp.ipAddressString, networkPrefixLength);
FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo(); FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo();
break; break;
} catch (IllegalArgumentException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
} else {
Log.i(TAG, "Getting subnet failed: " + 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,4 +1,4 @@
package org.fdroid.fdroid.nearby; package org.fdroid.fdroid.net.bluetooth;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothServerSocket;
@ -7,14 +7,14 @@ import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.nearby.httpish.Request; import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
import org.fdroid.fdroid.nearby.httpish.Response; import org.fdroid.fdroid.net.bluetooth.httpish.Request;
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -35,9 +35,18 @@ public class BluetoothServer extends Thread {
private final List<ClientConnection> clients = new ArrayList<>(); private final List<ClientConnection> clients = new ArrayList<>();
private final File webRoot; 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.webRoot = webRoot;
this.swap = swap;
start();
}
public boolean isRunning() {
return isRunning;
} }
public void close() { public void close() {
@ -56,12 +65,15 @@ public class BluetoothServer extends Thread {
@Override @Override
public void run() { public void run() {
isRunning = true;
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
try { try {
serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid()); serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now", e); Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now", e);
swap.stop();
isRunning = false;
return; return;
} }
@ -91,6 +103,7 @@ public class BluetoothServer extends Thread {
Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients", e); 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 { private static class ClientConnection extends Thread {
@ -144,7 +157,7 @@ public class BluetoothServer extends Thread {
Response.Builder builder = null; Response.Builder builder = null;
try { try {
int statusCode = HttpURLConnection.HTTP_NOT_FOUND; int statusCode = 404;
int totalSize = -1; int totalSize = -1;
if (request.getMethod().equals(Request.Methods.HEAD)) { if (request.getMethod().equals(Request.Methods.HEAD)) {

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

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