Compare commits
No commits in common. "master" and "1.3-alpha3" have entirely different histories.
master
...
1.3-alpha3
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
*.gpg binary
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
122
.gitlab-ci.yml
122
.gitlab-ci.yml
@ -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/
|
||||||
|
3
.weblate
3
.weblate
@ -1,3 +0,0 @@
|
|||||||
[weblate]
|
|
||||||
url = https://hosted.weblate.org/api/
|
|
||||||
translation = f-droid/f-droid
|
|
@ -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)
|
||||||
|
340
CHANGELOG.md
340
CHANGELOG.md
@ -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
|
||||||
|
@ -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.
|
||||||
|
11
FUNDING.yml
11
FUNDING.yml
@ -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
|
|
@ -1,6 +1,6 @@
|
|||||||
# F-Droid Client
|
# F-Droid Client
|
||||||
|
|
||||||
[](https://gitlab.com/fdroid/fdroidclient/-/jobs)
|
[](https://gitlab.com/fdroid/fdroidclient/builds)
|
||||||
[](https://hosted.weblate.org/engage/f-droid/)
|
[](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
23
RELEASE_CHECKLIST.md
Normal 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`
|
118
app/build.gradle
118
app/build.gradle
@ -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) {
|
||||||
|
20
app/lint.xml
20
app/lint.xml
@ -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>
|
||||||
|
|
||||||
|
28
app/proguard-rules.pro
vendored
28
app/proguard-rules.pro
vendored
@ -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>(...);
|
|
||||||
}
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package org.fdroid.fdroid;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see <a href="https://artemzin.com/blog/easiest-way-to-give-set_animation_scale-permission-for-your-ui-tests-on-android/>EASIEST WAY TO GIVE SET_ANIMATION_SCALE PERMISSION FOR YOUR UI TESTS ON ANDROID</a>
|
|
||||||
* @see <a href="https://gist.github.com/xrigau/11284124>Disable animations for Espresso tests</a>
|
|
||||||
*/
|
|
||||||
class SystemAnimations {
|
|
||||||
public static final String TAG = "SystemAnimations";
|
|
||||||
|
|
||||||
private static final float DISABLED = 0.0f;
|
|
||||||
private static final float DEFAULT = 1.0f;
|
|
||||||
|
|
||||||
static void disableAll(Context context) {
|
|
||||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
|
||||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
|
||||||
setSystemAnimationsScale(DISABLED);
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Disabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void enableAll(Context context) {
|
|
||||||
int permStatus = context.checkCallingOrSelfPermission(Manifest.permission.SET_ANIMATION_SCALE);
|
|
||||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
Log.i(TAG, "Manifest.permission.SET_ANIMATION_SCALE PERMISSION_GRANTED");
|
|
||||||
setSystemAnimationsScale(DEFAULT);
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Enabling Manifest.permission.SET_ANIMATION_SCALE failed: " + permStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setSystemAnimationsScale(float animationScale) {
|
|
||||||
try {
|
|
||||||
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
|
|
||||||
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
|
|
||||||
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
|
|
||||||
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
|
|
||||||
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
|
|
||||||
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
|
|
||||||
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
|
|
||||||
|
|
||||||
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
|
|
||||||
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
|
|
||||||
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
|
|
||||||
for (int i = 0; i < currentScales.length; i++) {
|
|
||||||
currentScales[i] = animationScale;
|
|
||||||
}
|
|
||||||
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Could not change animation scale to " + animationScale + " :'(");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
9
app/src/androidTest/proguard-rules.pro
vendored
9
app/src/androidTest/proguard-rules.pro
vendored
@ -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>(...);
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
package org.fdroid.fdroid.nearby;
|
|
||||||
|
|
||||||
public class LocalRepoManager {
|
|
||||||
public static final String[] WEB_ROOT_ASSET_FILES = {};
|
|
||||||
}
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
@ -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.
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 |
@ -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>
|
@ -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>
|
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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");
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
@ -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();
|
@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -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();
|
||||||
|
}
|
@ -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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
109
app/src/full/java/org/fdroid/fdroid/localrepo/type/SwapType.java
Normal file
109
app/src/full/java/org/fdroid/fdroid/localrepo/type/SwapType.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
181
app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
Normal file
181
app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(" <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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
406
app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
Normal file
406
app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
Normal 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(" <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();
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)) {
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
Loading…
x
Reference in New Issue
Block a user