Compare commits
No commits in common. "master" and "db-version/21" have entirely different histories.
master
...
db-version
8
.android2po
Normal file
8
.android2po
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
--gettext locale/
|
||||||
|
--groups strings array
|
||||||
|
|
||||||
|
--ignore about_sitec about_mailc
|
||||||
|
--ignore repo_add_http
|
||||||
|
--ignore /updateIntervalValues.*/
|
||||||
|
--ignore /dbSyncModeValues.*/
|
||||||
|
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
*.gpg binary
|
|
53
.gitignore
vendored
53
.gitignore
vendored
@ -1,49 +1,12 @@
|
|||||||
# Built application files
|
|
||||||
*.apk
|
|
||||||
*.ap_
|
|
||||||
|
|
||||||
# Files for the Dalvik VM
|
|
||||||
*.dex
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
build.xml
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
local.properties
|
||||||
|
build.properties
|
||||||
# Proguard folder generated by Eclipse
|
project.properties
|
||||||
proguard/
|
.classpath
|
||||||
|
bin/*
|
||||||
# Log Files
|
gen/*
|
||||||
*.log
|
proguard.cfg
|
||||||
|
proguard-project.txt
|
||||||
# Editor swap/save files
|
|
||||||
*~
|
*~
|
||||||
*.swp
|
.idea
|
||||||
|
|
||||||
# More IDE stuff
|
|
||||||
.idea/
|
|
||||||
*.iml
|
*.iml
|
||||||
out
|
out
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Imported libs
|
|
||||||
extern/*/libs/
|
|
||||||
extern/*/*/libs/
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
junit-report.xml
|
|
||||||
|
|
||||||
# Screen dumps from Android Studio/DDMS
|
|
||||||
captures/
|
|
||||||
|
|
||||||
/fdroid/
|
|
||||||
|
142
.gitlab-ci.yml
142
.gitlab-ci.yml
@ -1,142 +0,0 @@
|
|||||||
stages:
|
|
||||||
- test
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
.base:
|
|
||||||
image: registry.gitlab.com/fdroid/ci-images-client:latest
|
|
||||||
before_script:
|
|
||||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
|
||||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
|
|
||||||
- alias sdkmanager="sdkmanager --no_https"
|
|
||||||
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
|
|
||||||
# limit RAM usage for all gradle runs
|
|
||||||
- export maxmem=$(expr $(sed -n 's,^MemAvailable:[^0-9]*\([0-9][0-9]*\)[^0-9]*$,\1,p' /proc/meminfo) / 1024 / 2 / 1024 \* 1024)
|
|
||||||
- printf "\norg.gradle.jvmargs=-Xmx${maxmem}m -XX:MaxPermSize=${maxmem}m\norg.gradle.daemon=false\norg.gradle.parallel=false\n" >> gradle.properties
|
|
||||||
after_script:
|
|
||||||
# this file changes every time but should not be cached
|
|
||||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
|
||||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .gradle/wrapper
|
|
||||||
- .gradle/caches
|
|
||||||
|
|
||||||
.test-template: &test-template
|
|
||||||
extends: .base
|
|
||||||
stage: test
|
|
||||||
artifacts:
|
|
||||||
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
|
|
||||||
paths:
|
|
||||||
- kernel.log
|
|
||||||
- logcat.txt
|
|
||||||
- app/core*
|
|
||||||
- app/*.log
|
|
||||||
- app/build/reports
|
|
||||||
- app/build/outputs/*ml
|
|
||||||
- app/build/outputs/apk
|
|
||||||
expire_in: 1 week
|
|
||||||
when: on_failure
|
|
||||||
after_script:
|
|
||||||
- echo "Download debug artifacts from https://gitlab.com/${CI_PROJECT_PATH}/-/jobs"
|
|
||||||
|
|
||||||
# Run the most important first. Then we can decide whether to ignore
|
|
||||||
# the style tests if the rest of the more meaningful tests pass.
|
|
||||||
test_lint_pmd_checkstyle:
|
|
||||||
<<: *test-template
|
|
||||||
script:
|
|
||||||
- export EXITVALUE=0
|
|
||||||
- function set_error() { export EXITVALUE=1; printf "\x1b[31mERROR `history|tail -2|head -1|cut -b 6-500`\x1b[0m\n"; }
|
|
||||||
- ./gradlew assemble
|
|
||||||
# always report on lint errors to the build log
|
|
||||||
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle
|
|
||||||
- ./gradlew testFullDebugUnitTest || set_error
|
|
||||||
- ./gradlew lint || set_error
|
|
||||||
- ./gradlew pmd || set_error
|
|
||||||
- ./gradlew checkstyle || set_error
|
|
||||||
- ./tools/check-format-strings.py || set_error
|
|
||||||
- ./tools/check-fastlane-whitespace.py || set_error
|
|
||||||
- ./tools/remove-unused-and-blank-translations.py || set_error
|
|
||||||
- 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
|
|
||||||
- exit $EXITVALUE
|
|
||||||
|
|
||||||
errorprone:
|
|
||||||
extends: .base
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install -t stretch-backports openjdk-11-jdk-headless
|
|
||||||
- update-java-alternatives --set java-1.11.0-openjdk-amd64
|
|
||||||
- export JAVA_HOME=/usr/lib/jvm/java-1.11.0-openjdk-amd64
|
|
||||||
- cat config/errorprone.gradle >> app/build.gradle
|
|
||||||
- ./gradlew -Dorg.gradle.dependency.verification=lenient assembleDebug
|
|
||||||
|
|
||||||
# Run the tests in the emulator. Each step is broken out to run on
|
|
||||||
# its own since the CI runner can have limited RAM, and the emulator
|
|
||||||
# can take a while to start.
|
|
||||||
#
|
|
||||||
# once these prove stable, the task should be switched to
|
|
||||||
# connectedCheck to test all the build flavors
|
|
||||||
.connected-template: &connected-template
|
|
||||||
extends: .base
|
|
||||||
script:
|
|
||||||
- ./gradlew assembleFullDebug
|
|
||||||
- export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'`
|
|
||||||
- export AVD_TAG=`echo $CI_JOB_NAME | awk '{print $3}'`
|
|
||||||
- export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'`
|
|
||||||
- export AVD_PACKAGE="system-images;android-${AVD_SDK};${AVD_TAG};${AVD_ARCH}"
|
|
||||||
- echo $AVD_PACKAGE
|
|
||||||
|
|
||||||
- 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
|
|
||||||
- ./gradlew connectedFullDebugAndroidTest $FLAG
|
|
||||||
|
|
||||||
no-accel 22 default x86:
|
|
||||||
<<: *test-template
|
|
||||||
<<: *connected-template
|
|
||||||
|
|
||||||
.kvm-template: &kvm-template
|
|
||||||
tags:
|
|
||||||
- fdroid
|
|
||||||
- kvm
|
|
||||||
only:
|
|
||||||
variables:
|
|
||||||
- $RUN_KVM_JOBS
|
|
||||||
<<: *test-template
|
|
||||||
<<: *connected-template
|
|
||||||
|
|
||||||
kvm 29 microg x86_64:
|
|
||||||
<<: *kvm-template
|
|
||||||
|
|
||||||
deploy_nightly:
|
|
||||||
extends: .base
|
|
||||||
stage: deploy
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
script:
|
|
||||||
- test -z "$DEBUG_KEYSTORE" && exit 0
|
|
||||||
- sed -i
|
|
||||||
's,<string name="app_name">.*</string>,<string name="app_name">F-Nightly</string>,'
|
|
||||||
app/src/main/res/values*/strings.xml
|
|
||||||
# add this nightly repo as a enabled repo
|
|
||||||
- sed -i -e '/<\/string-array>/d' -e '/<\/resources>/d' app/src/main/res/values/default_repos.xml
|
|
||||||
- echo "<item>${CI_PROJECT_PATH}-nightly</item>" >> app/src/main/res/values/default_repos.xml
|
|
||||||
- echo "<item>${CI_PROJECT_URL}-nightly/raw/master/fdroid/repo</item>" >> app/src/main/res/values/default_repos.xml
|
|
||||||
- cat config/nightly-repo/repo.xml >> app/src/main/res/values/default_repos.xml
|
|
||||||
- export DB=`sed -n 's,.*DB_VERSION *= *\([0-9][0-9]*\).*,\1,p' app/src/main/java/org/fdroid/fdroid/data/DBHelper.java`
|
|
||||||
- export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b4-8)`
|
|
||||||
- sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," app/build.gradle
|
|
||||||
# build the APKs!
|
|
||||||
- ./gradlew assembleDebug
|
|
||||||
- fdroid nightly -v
|
|
33
.project
Normal file
33
.project
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>fdroid</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
3
.weblate
3
.weblate
@ -1,3 +0,0 @@
|
|||||||
[weblate]
|
|
||||||
url = https://hosted.weblate.org/api/
|
|
||||||
translation = f-droid/f-droid
|
|
26
Android.mk
26
Android.mk
@ -1,26 +1,8 @@
|
|||||||
LOCAL_PATH:= $(call my-dir)
|
LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
|
|
||||||
LOCAL_MODULE := F-Droid
|
LOCAL_PACKAGE_NAME := FDroid
|
||||||
LOCAL_MODULE_TAGS := optional
|
LOCAL_SRC_FILES := $(call all-java-files-under,src)
|
||||||
LOCAL_PACKAGE_NAME := F-Droid
|
|
||||||
|
|
||||||
fdroid_root := $(LOCAL_PATH)
|
include $(BUILD_PACKAGE)
|
||||||
fdroid_dir := app
|
|
||||||
fdroid_out := $(PWD)/$(OUT_DIR)/target/common/obj/APPS/$(LOCAL_MODULE)_intermediates
|
|
||||||
fdroid_build := $(fdroid_root)/$(fdroid_dir)/build
|
|
||||||
fdroid_apk := build/outputs/apk/full/release/$(fdroid_dir)-full-release-unsigned.apk
|
|
||||||
|
|
||||||
$(fdroid_root)/$(fdroid_dir)/$(fdroid_apk):
|
|
||||||
rm -Rf $(fdroid_build)
|
|
||||||
mkdir -p $(fdroid_out)
|
|
||||||
ln -sf $(fdroid_out) $(fdroid_build)
|
|
||||||
cd $(fdroid_root)/$(fdroid_dir) && gradle assembleRelease
|
|
||||||
|
|
||||||
LOCAL_CERTIFICATE := platform
|
|
||||||
LOCAL_SRC_FILES := $(fdroid_dir)/$(fdroid_apk)
|
|
||||||
LOCAL_MODULE_CLASS := APPS
|
|
||||||
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
|
|
||||||
|
|
||||||
include $(BUILD_PREBUILT)
|
|
||||||
|
92
AndroidManifest.xml
Normal file
92
AndroidManifest.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.fdroid.fdroid"
|
||||||
|
android:installLocation="auto"
|
||||||
|
android:versionCode="45"
|
||||||
|
android:versionName="0.45" >
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="3"
|
||||||
|
android:targetSdkVersion="15" />
|
||||||
|
|
||||||
|
<supports-screens
|
||||||
|
android:anyDensity="true"
|
||||||
|
android:largeScreens="true"
|
||||||
|
android:normalScreens="true"
|
||||||
|
android:resizeable="true"
|
||||||
|
android:smallScreens="true"
|
||||||
|
android:xlargeScreens="true" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name="FDroidApp"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name" >
|
||||||
|
<activity
|
||||||
|
android:name="FDroid"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.default_searchable"
|
||||||
|
android:value=".SearchResults" />
|
||||||
|
</activity>
|
||||||
|
<activity android:name="ManageRepo" />
|
||||||
|
<activity android:name="Settings" />
|
||||||
|
<activity
|
||||||
|
android:name="AppDetails"
|
||||||
|
android:exported="true" >
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<data android:scheme="fdroid.app" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity android:name="Preferences" />
|
||||||
|
<activity
|
||||||
|
android:name="SearchResults"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/searchable" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<receiver android:name="StartupReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<receiver android:name="PackageReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||||
|
<action android:name="android.intent.action.PACKAGE_UPGRADED" />
|
||||||
|
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||||
|
|
||||||
|
<data android:scheme="package" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service android:name="UpdateService" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
1081
CHANGELOG.md
1081
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,81 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
## Reporting issues
|
|
||||||
|
|
||||||
If you find an issue in the client, you can use our [Issue
|
|
||||||
Tracker](https://gitlab.com/fdroid/fdroidclient/issues). Make sure that it
|
|
||||||
hasn't yet been reported by searching first.
|
|
||||||
|
|
||||||
Remember to include the following information:
|
|
||||||
|
|
||||||
* Android version
|
|
||||||
* Device model
|
|
||||||
* F-Droid version
|
|
||||||
* Steps to reproduce the issue
|
|
||||||
* Logcat - see [instructions](https://f-droid.org/wiki/page/Getting_logcat_messages_after_crash)
|
|
||||||
|
|
||||||
## Translating
|
|
||||||
|
|
||||||
The strings are translated using [Weblate](https://weblate.org/en/). Follow
|
|
||||||
[these instructions](https://hosted.weblate.org/engage/f-droid/) if you would
|
|
||||||
like to contribute.
|
|
||||||
|
|
||||||
Please *do not* send merge requests or patches modifying the translations. Use
|
|
||||||
Weblate instead - it applies a series of fixes and suggestions, plus it keeps
|
|
||||||
track of modifications and fuzzy translations. Applying translations manually
|
|
||||||
skips all of the fixes and checks, and overrides the fuzzy state of strings.
|
|
||||||
|
|
||||||
Note that you cannot change the English strings on Weblate. If you have any
|
|
||||||
suggestions on how to improve them, open an issue or merge request like you
|
|
||||||
would if you were making code changes. This way the changes can be reviewed
|
|
||||||
before the source strings on Weblate are changed.
|
|
||||||
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This
|
|
||||||
should be more or less the same as [Android Java
|
|
||||||
style](https://source.android.com/source/code-style.html). Some key points:
|
|
||||||
|
|
||||||
* Four space indentation
|
|
||||||
* UTF-8 source files
|
|
||||||
* Exactly one top-level class per file
|
|
||||||
* No wildcard imports
|
|
||||||
* One statement per line
|
|
||||||
* K&R spacings with braces and parenthesis
|
|
||||||
* Commented fallthroughs
|
|
||||||
* Braces are always used after if, for and while
|
|
||||||
|
|
||||||
The current code base doesn't follow it entirely, but new code should follow
|
|
||||||
it. We enforce some of these, but not all, via `./gradlew checkstyle`.
|
|
||||||
|
|
||||||
|
|
||||||
## Running the test suite
|
|
||||||
|
|
||||||
Before pushing commits to a merge request, make sure this passes:
|
|
||||||
|
|
||||||
./gradlew checkstyle pmd lint
|
|
||||||
|
|
||||||
In order to run the F-Droid test suite, you will need to have either a real device
|
|
||||||
connected via `adb`, or an emulator running. Then, execute the following from the
|
|
||||||
command line:
|
|
||||||
|
|
||||||
./gradlew check
|
|
||||||
|
|
||||||
Many important tests require a device or emulator, but do not work in GitLab CI.
|
|
||||||
That mean they need to be run locally, and that is usually easiest in Android
|
|
||||||
Studio rather than the command line.
|
|
||||||
|
|
||||||
For a quick way to run a specific JUnit/Robolectric test:
|
|
||||||
|
|
||||||
./gradlew testFullDebugUnitTest --tests *LocaleSelectionTest*
|
|
||||||
|
|
||||||
For a quick way to run a specific emulator test:
|
|
||||||
|
|
||||||
./gradlew connectedFullDebugAndroidTest \
|
|
||||||
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest
|
|
||||||
|
|
||||||
|
|
||||||
## Making releases
|
|
||||||
|
|
||||||
See https://gitlab.com/fdroid/wiki/-/wikis/Internal/Release-Process#fdroidclient
|
|
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
|
|
67
README.md
67
README.md
@ -1,67 +0,0 @@
|
|||||||
# F-Droid Client
|
|
||||||
|
|
||||||
[](https://gitlab.com/fdroid/fdroidclient/-/jobs)
|
|
||||||
[](https://hosted.weblate.org/engage/f-droid/)
|
|
||||||
|
|
||||||
Client for [F-Droid](https://f-droid.org), the Free Software repository system
|
|
||||||
for Android.
|
|
||||||
|
|
||||||
## Building with Gradle
|
|
||||||
|
|
||||||
./gradlew assembleRelease
|
|
||||||
|
|
||||||
## Direct download
|
|
||||||
|
|
||||||
You can [download the application](https://f-droid.org/FDroid.apk) directly
|
|
||||||
from our site or [browse it in the repo](https://f-droid.org/app/org.fdroid.fdroid).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
See our [Contributing doc](CONTRIBUTING.md) for information on how to report
|
|
||||||
issues, translate the app into your language or help with development.
|
|
||||||
|
|
||||||
## IRC
|
|
||||||
|
|
||||||
We are on `#fdroid` and `#fdroid-dev` on Freenode. We hold weekly dev meetings
|
|
||||||
on `#fdroid-dev` on Thursdays at 11:30h UTC, which usually last half an hour.
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
* Why does F-Droid require "Unknown Sources" to install apps by default?
|
|
||||||
|
|
||||||
Because a regular Android app cannot act as a package manager on its
|
|
||||||
own. To do so, it would require system privileges (see below), similar
|
|
||||||
to what Google Play does.
|
|
||||||
|
|
||||||
* Can I avoid enabling "Unknown Sources" by installing F-Droid as a
|
|
||||||
privileged system app?
|
|
||||||
|
|
||||||
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
|
|
||||||
the system. It can be bundled with a ROM or installed via a zip.
|
|
||||||
## License
|
|
||||||
|
|
||||||
This program is Free Software: You can use, study share and improve it at your
|
|
||||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
|
||||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
|
||||||
published by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Some icons are made by [Picol](http://www.flaticon.com/authors/picol),
|
|
||||||
[Icomoon](http://www.flaticon.com/authors/icomoon) or
|
|
||||||
[Dave Gandy](http://www.flaticon.com/authors/dave-gandy) from
|
|
||||||
[Flaticon](http://www.flaticon.com) or by Google and are licensed by
|
|
||||||
[Creative Commons BY 3.0](https://creativecommons.org/licenses/by/3.0/).
|
|
||||||
|
|
||||||
Other icons are from the
|
|
||||||
[Material Design Icon set](https://github.com/google/material-design-icons)
|
|
||||||
released under an
|
|
||||||
[Attribution 4.0 International license](https://creativecommons.org/licenses/by/4.0/).
|
|
||||||
|
|
||||||
|
|
||||||
## Translation
|
|
||||||
|
|
||||||
Everything can be translated. See
|
|
||||||
[Translation and Localization](https://f-droid.org/docs/Translation_and_Localization)
|
|
||||||
for more info.
|
|
||||||
[](https://hosted.weblate.org/engage/f-droid/?utm_source=widget)
|
|
18
ant.properties
Normal file
18
ant.properties
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# This file is used to override default values used by the Ant build system.
|
||||||
|
#
|
||||||
|
# This file must be checked into Version Control Systems, as it is
|
||||||
|
# integral to the build system of your project.
|
||||||
|
|
||||||
|
# This file is only used by the Ant script.
|
||||||
|
|
||||||
|
# You can use this to override default values such as
|
||||||
|
# 'source.dir' for the location of your java source folder and
|
||||||
|
# 'out.dir' for the location of your output folder.
|
||||||
|
|
||||||
|
# You can also use it define how the release builds are signed by declaring
|
||||||
|
# the following properties:
|
||||||
|
# 'key.store' for the location of your keystore and
|
||||||
|
# 'key.alias' for the name of the key to use.
|
||||||
|
# The password will be asked during the build when you use the 'release' target.
|
||||||
|
|
||||||
|
application.package=org.fdroid.fdroid
|
230
app/build.gradle
230
app/build.gradle
@ -1,230 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'checkstyle'
|
|
||||||
apply plugin: 'pmd'
|
|
||||||
|
|
||||||
/* gets the version name from the latest Git tag */
|
|
||||||
def getVersionName = { ->
|
|
||||||
def stdout = new ByteArrayOutputStream()
|
|
||||||
exec {
|
|
||||||
commandLine 'git', 'describe', '--tags', '--always'
|
|
||||||
standardOutput = stdout
|
|
||||||
}
|
|
||||||
return stdout.toString().trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
def isCi = "true" == System.getenv("CI")
|
|
||||||
def preDexEnabled = "true" == System.getProperty("pre-dex", "true")
|
|
||||||
|
|
||||||
def fullApplicationId = "org.fdroid.fdroid"
|
|
||||||
def basicApplicationId = "org.fdroid.basic"
|
|
||||||
// yes, this actually needs both quotes https://stackoverflow.com/a/41391841
|
|
||||||
def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 30
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
versionCode 1013001
|
|
||||||
versionName getVersionName()
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// use proguard on debug too since we have unknowingly broken
|
|
||||||
// release builds before.
|
|
||||||
all {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
buildConfigField "String", "PRIVILEGED_EXTENSION_PACKAGE_NAME", privilegedExtensionApplicationId
|
|
||||||
buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"' // String needs both quotes
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro'
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
resValue "string", "applicationId", fullApplicationId + applicationIdSuffix
|
|
||||||
versionNameSuffix "-debug"
|
|
||||||
println 'buildTypes.debug defaultConfig.versionCode ' + defaultConfig.versionCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "base"
|
|
||||||
productFlavors {
|
|
||||||
full {
|
|
||||||
dimension "base"
|
|
||||||
applicationId fullApplicationId
|
|
||||||
resValue "string", "applicationId", fullApplicationId
|
|
||||||
}
|
|
||||||
basic {
|
|
||||||
dimension "base"
|
|
||||||
applicationId basicApplicationId
|
|
||||||
resValue "string", "applicationId", basicApplicationId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
compileOptions.encoding = "UTF-8"
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
aaptOptions {
|
|
||||||
cruncherEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
// Improve build server performance by allowing disabling of pre-dexing
|
|
||||||
// see http://tools.android.com/tech-docs/new-build-system/tips#TOC-Improving-Build-Server-performance
|
|
||||||
// Skip pre-dexing when running on CI or when disabled via -Dpre-dex=false.
|
|
||||||
preDexLibraries = preDexEnabled && !isCi
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests {
|
|
||||||
includeAndroidResources = true
|
|
||||||
// prevent tests from dying on android.util.Log calls
|
|
||||||
returnDefaultValues = true
|
|
||||||
all {
|
|
||||||
// All the usual Gradle options.
|
|
||||||
testLogging {
|
|
||||||
events "skipped", "failed", "standardOut", "standardError"
|
|
||||||
showStandardStreams = true
|
|
||||||
}
|
|
||||||
systemProperty 'robolectric.dependency.repo.url', 'https://repo1.maven.org/maven2'
|
|
||||||
|
|
||||||
// hack to avoid memory leak crashes
|
|
||||||
forkEvery = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
test {
|
|
||||||
java.srcDirs += "$projectDir/src/testShared/java"
|
|
||||||
}
|
|
||||||
|
|
||||||
androidTest {
|
|
||||||
java.srcDirs += "$projectDir/src/testShared/java"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
checkReleaseBuilds false
|
|
||||||
abortOnError true
|
|
||||||
|
|
||||||
htmlReport true
|
|
||||||
xmlReport false
|
|
||||||
textReport false
|
|
||||||
|
|
||||||
lintConfig file("lint.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/LICENSE.txt'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
exclude 'META-INF/NOTICE.txt'
|
|
||||||
exclude 'META-INF/INDEX.LIST'
|
|
||||||
exclude '.readme'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.palette:palette:1.0.0'
|
|
||||||
implementation 'androidx.work:work-runtime:2.4.0'
|
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
|
||||||
|
|
||||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
|
||||||
implementation 'com.google.zxing:core:3.3.3'
|
|
||||||
implementation 'info.guardianproject.netcipher:netcipher:2.2.0-alpha'
|
|
||||||
implementation 'info.guardianproject.panic:panic:1.0'
|
|
||||||
implementation 'commons-io:commons-io:2.6'
|
|
||||||
implementation 'commons-net:commons-net:3.6'
|
|
||||||
implementation 'ch.acra:acra:4.9.1'
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1'
|
|
||||||
|
|
||||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
|
||||||
implementation 'io.reactivex.rxjava3:rxjava:3.0.9'
|
|
||||||
|
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1'
|
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1'
|
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
|
|
||||||
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
|
||||||
fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
|
|
||||||
fullImplementation 'cc.mvdan.accesspoint:library:0.2.0'
|
|
||||||
fullImplementation 'org.jmdns:jmdns:3.5.5'
|
|
||||||
fullImplementation 'org.nanohttpd:nanohttpd:2.3.1'
|
|
||||||
|
|
||||||
testImplementation 'androidx.test:core:1.3.0'
|
|
||||||
testImplementation 'junit:junit:4.13.1'
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.3'
|
|
||||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
|
||||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
|
||||||
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
|
|
||||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:monitor:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
|
||||||
androidTestImplementation 'androidx.work:work-testing:2.4.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
checkstyle {
|
|
||||||
toolVersion = '7.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
task checkstyle(type: Checkstyle) {
|
|
||||||
configFile file("${project.rootDir}/config/checkstyle/checkstyle.xml")
|
|
||||||
source 'src/main/java', 'src/test/java', 'src/androidTest/java'
|
|
||||||
include '**/*.java'
|
|
||||||
|
|
||||||
classpath = files()
|
|
||||||
}
|
|
||||||
|
|
||||||
pmd {
|
|
||||||
toolVersion = '6.20.0'
|
|
||||||
consoleOutput = true
|
|
||||||
}
|
|
||||||
|
|
||||||
task pmdMain(type: Pmd) {
|
|
||||||
dependsOn 'assembleDebug'
|
|
||||||
ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-main.xml")
|
|
||||||
ruleSets = [] // otherwise defaults clash with the list in rules.xml
|
|
||||||
source 'src/main/java'
|
|
||||||
include '**/*.java'
|
|
||||||
}
|
|
||||||
|
|
||||||
task pmdTest(type: Pmd) {
|
|
||||||
dependsOn 'assembleDebug'
|
|
||||||
ruleSetFiles = files("${project.rootDir}/config/pmd/rules.xml", "${project.rootDir}/config/pmd/rules-test.xml")
|
|
||||||
ruleSets = [] // otherwise defaults clash with the list in rules.xml
|
|
||||||
source 'src/test/java', 'src/androidTest/java'
|
|
||||||
include '**/*.java'
|
|
||||||
}
|
|
||||||
|
|
||||||
task pmd(dependsOn: [pmdMain, pmdTest]) {}
|
|
67
app/lint.xml
67
app/lint.xml
@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<lint>
|
|
||||||
<!-- Our translations are crowd-sourced -->
|
|
||||||
<issue id="MissingTranslation" severity="ignore"/>
|
|
||||||
|
|
||||||
<!-- to make CI fail on errors until this is fixed
|
|
||||||
https://github.com/rtyley/spongycastle/issues/7 -->
|
|
||||||
<issue id="InvalidPackage" severity="warning"/>
|
|
||||||
|
|
||||||
<issue id="ImpliedQuantity" 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 -->
|
|
||||||
<issue id="UnusedResources" severity="error">
|
|
||||||
<ignore path="src/main/res/drawable/category_**.png" />
|
|
||||||
<ignore path="src/main/res/values/dimens.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 id="AppCompatMethod" severity="error"/>
|
|
||||||
<issue id="NestedScrolling" severity="error"/>
|
|
||||||
<issue id="Typos" severity="error"/>
|
|
||||||
<issue id="StringFormatCount" 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="HardcodedText" severity="error"/>
|
|
||||||
<issue id="RtlCompat" severity="error"/>
|
|
||||||
<issue id="RtlEnabled" severity="error"/>
|
|
||||||
|
|
||||||
<!-- both the correct and deprecated locales need to be present for
|
|
||||||
them to be recognized on all devices -->
|
|
||||||
<issue id="LocaleFolder" severity="error">
|
|
||||||
<ignore path="src/main/res/values-he"/>
|
|
||||||
<ignore path="src/main/res/values-id"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue id="SetWorldReadable" severity="error">
|
|
||||||
<ignore path="src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue id="ProtectedPermissions" severity="error">
|
|
||||||
<ignore path="src/debug/AndroidManifest.xml"/>
|
|
||||||
<ignore path="src/full/AndroidManifest.xml"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<!-- these should be fixed, but it'll be a chunk of work -->
|
|
||||||
<issue id="SetTextI18n" severity="error">
|
|
||||||
<ignore path="src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java"/>
|
|
||||||
<ignore path="src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java"/>
|
|
||||||
</issue>
|
|
||||||
</lint>
|
|
47
app/proguard-rules.pro
vendored
47
app/proguard-rules.pro
vendored
@ -1,47 +0,0 @@
|
|||||||
-dontobfuscate
|
|
||||||
-dontoptimize
|
|
||||||
-keepattributes SourceFile,LineNumberTable,Exceptions
|
|
||||||
-keep class org.fdroid.fdroid.** {*;}
|
|
||||||
-dontskipnonpubliclibraryclassmembers
|
|
||||||
-dontwarn android.test.**
|
|
||||||
|
|
||||||
-dontwarn javax.naming.**
|
|
||||||
-dontwarn org.slf4j.**
|
|
||||||
-dontnote org.apache.http.**
|
|
||||||
-dontnote android.net.http.**
|
|
||||||
-dontnote **ILicensingService
|
|
||||||
|
|
||||||
# Needed for espresso https://stackoverflow.com/a/21706087
|
|
||||||
-dontwarn org.xmlpull.v1.**
|
|
||||||
|
|
||||||
# StrongHttpsClient and its support classes are totally unused, so the
|
|
||||||
# ch.boye.httpclientandroidlib.** classes are also unneeded
|
|
||||||
-dontwarn info.guardianproject.netcipher.client.**
|
|
||||||
|
|
||||||
# These libraries are known to break if minification is enabled on them. They
|
|
||||||
# use reflection to instantiate classes, for example. If the keep flags are
|
|
||||||
# removed, proguard will strip classes which are required, which may result in
|
|
||||||
# crashes.
|
|
||||||
-keep class kellinwood.security.zipsigner.** {*;}
|
|
||||||
-keep class org.bouncycastle.** {*;}
|
|
||||||
|
|
||||||
# This keeps class members used for SystemInstaller IPC.
|
|
||||||
# Reference: https://gitlab.com/fdroid/fdroidclient/issues/79
|
|
||||||
-keepclassmembers class * implements android.os.IInterface {
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepattributes *Annotation*,EnclosingMethod,Signature
|
|
||||||
-keepnames class com.fasterxml.jackson.** { *; }
|
|
||||||
-dontwarn com.fasterxml.jackson.databind.ext.**
|
|
||||||
-keep class org.codehaus.** { *; }
|
|
||||||
-keepclassmembers public final enum org.codehaus.jackson.annotate.JsonAutoDetect$Visibility {
|
|
||||||
public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *; }
|
|
||||||
-keep public class your.class.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
|
|
||||||
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
|
|
||||||
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
|
|
||||||
public <init>(...);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="org.fdroid.fdroid.tests"
|
|
||||||
android:versionCode="1"
|
|
||||||
android:versionName="1.0">
|
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
|
|
||||||
|
|
||||||
<!-- We add an application tag here just so that we can indicate that
|
|
||||||
this package needs to link against the android.test library,
|
|
||||||
which is needed when building test cases. -->
|
|
||||||
<application>
|
|
||||||
<uses-library
|
|
||||||
android:name="android.test.runner"
|
|
||||||
android:required="false" />
|
|
||||||
</application>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
|
|
||||||
</manifest>
|
|
@ -1,125 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<fdroid>
|
|
||||||
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
|
|
||||||
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
|
|
||||||
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
|
|
||||||
<description>
|
|
||||||
This is just a test of the extended permissions attributes.
|
|
||||||
</description>
|
|
||||||
</repo>
|
|
||||||
|
|
||||||
<application id="urzip.at.or.at.urzip">
|
|
||||||
<id>at.bitfire.davdroid</id>
|
|
||||||
<added>2013-10-13</added>
|
|
||||||
<lastupdated>2016-06-26</lastupdated>
|
|
||||||
<name>DAVdroid</name>
|
|
||||||
<summary>Contacts and Calendar sync</summary>
|
|
||||||
<icon>at.bitfire.davdroid.107.png</icon>
|
|
||||||
<desc>apk generated from urzip to test extended permissions</desc>
|
|
||||||
<license>GPLv3</license>
|
|
||||||
<categories>Internet</categories>
|
|
||||||
<category>Internet</category>
|
|
||||||
<web>https://davdroid.bitfire.at/</web>
|
|
||||||
<source>https://davdroid.bitfire.at/source/</source>
|
|
||||||
<tracker>https://davdroid.bitfire.at/forums/</tracker>
|
|
||||||
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
|
|
||||||
<donate>https://davdroid.bitfire.at/donate/</donate>
|
|
||||||
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
|
|
||||||
<flattr>2100160</flattr>
|
|
||||||
<marketversion>1.1.1.2</marketversion>
|
|
||||||
<marketvercode>107</marketvercode>
|
|
||||||
<package>
|
|
||||||
<version>1.3.2-FAKE</version>
|
|
||||||
<versioncode>117</versioncode>
|
|
||||||
<apkname>org.fdroid.extendedpermissionstest.apk</apkname>
|
|
||||||
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
|
|
||||||
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
|
|
||||||
</hash>
|
|
||||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
|
||||||
<size>3298864</size>
|
|
||||||
<sdkver>14</sdkver>
|
|
||||||
<targetSdkVersion>23</targetSdkVersion>
|
|
||||||
<added>2016-09-22</added>
|
|
||||||
<permissions>
|
|
||||||
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,READ_SYNC_STATS
|
|
||||||
</permissions>
|
|
||||||
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
|
||||||
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
|
||||||
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
<uses-permission-sdk-23 name="android.permission.CAMERA" />
|
|
||||||
<uses-permission-sdk-23 name="android.permission.CALL_PHONE" maxSdkVersion="23" />
|
|
||||||
</package>
|
|
||||||
<package>
|
|
||||||
<version>1.3.1-ose</version>
|
|
||||||
<versioncode>116</versioncode>
|
|
||||||
<apkname>at.bitfire.davdroid_116.apk</apkname>
|
|
||||||
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
|
|
||||||
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
|
|
||||||
</hash>
|
|
||||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
|
||||||
<size>3298864</size>
|
|
||||||
<sdkver>14</sdkver>
|
|
||||||
<targetSdkVersion>24</targetSdkVersion>
|
|
||||||
<added>2016-09-21</added>
|
|
||||||
<permissions>
|
|
||||||
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
|
|
||||||
</permissions>
|
|
||||||
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
|
|
||||||
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
|
|
||||||
</package>
|
|
||||||
<package>
|
|
||||||
<version>1.1.1.2</version>
|
|
||||||
<versioncode>107</versioncode>
|
|
||||||
<apkname>at.bitfire.davdroid_107.apk</apkname>
|
|
||||||
<srcname>at.bitfire.davdroid_107_src.tar.gz</srcname>
|
|
||||||
<hash type="sha256">9a616f2e97bf8cf012baf896f95667dea4e3ce3252b31c5715073638a9fcc3d4
|
|
||||||
</hash>
|
|
||||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
|
||||||
<size>3134363</size>
|
|
||||||
<sdkver>14</sdkver>
|
|
||||||
<targetSdkVersion>23</targetSdkVersion>
|
|
||||||
<added>2016-06-26</added>
|
|
||||||
<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
|
|
||||||
</permissions>
|
|
||||||
</package>
|
|
||||||
<package>
|
|
||||||
<version>1.1.1.1</version>
|
|
||||||
<versioncode>105</versioncode>
|
|
||||||
<apkname>at.bitfire.davdroid_105.apk</apkname>
|
|
||||||
<srcname>at.bitfire.davdroid_105_src.tar.gz</srcname>
|
|
||||||
<hash type="sha256">4a0408c61536a1cc1028cea4273adbde2e57dfa2b12d93c3b52f4c3d095e2849
|
|
||||||
</hash>
|
|
||||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
|
||||||
<size>3131567</size>
|
|
||||||
<sdkver>14</sdkver>
|
|
||||||
<targetSdkVersion>23</targetSdkVersion>
|
|
||||||
<added>2016-06-24</added>
|
|
||||||
<permissions>
|
|
||||||
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>
|
|
||||||
</package>
|
|
||||||
<package>
|
|
||||||
<version>1.1.1</version>
|
|
||||||
<versioncode>104</versioncode>
|
|
||||||
<apkname>at.bitfire.davdroid_104.apk</apkname>
|
|
||||||
<srcname>at.bitfire.davdroid_104_src.tar.gz</srcname>
|
|
||||||
<hash type="sha256">09ba34996177efe8b1498a93fe6521ab84efab3bccb0f42449116e80b59e5b56
|
|
||||||
</hash>
|
|
||||||
<sig>03542175324d067b4c36582242f8aecc</sig>
|
|
||||||
<size>3131367</size>
|
|
||||||
<sdkver>14</sdkver>
|
|
||||||
<targetSdkVersion>23</targetSdkVersion>
|
|
||||||
<added>2016-06-22</added>
|
|
||||||
<permissions>
|
|
||||||
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>
|
|
||||||
</package>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</fdroid>
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,43 +0,0 @@
|
|||||||
package org.fdroid.fdroid;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
public class AssetUtils {
|
|
||||||
|
|
||||||
private static final String TAG = "Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static File copyAssetToDir(Context context, String assetName, File directory) {
|
|
||||||
File tempFile = null;
|
|
||||||
InputStream input = null;
|
|
||||||
OutputStream output = null;
|
|
||||||
try {
|
|
||||||
tempFile = File.createTempFile(assetName, ".testasset", directory);
|
|
||||||
Log.i(TAG, "Copying asset file " + assetName + " to directory " + directory);
|
|
||||||
input = context.getAssets().open(assetName);
|
|
||||||
output = new FileOutputStream(tempFile);
|
|
||||||
Utils.copy(input, output);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Check the context is from Instrumentation.getContext()");
|
|
||||||
fail(e.getMessage());
|
|
||||||
} finally {
|
|
||||||
Utils.closeQuietly(output);
|
|
||||||
Utils.closeQuietly(input);
|
|
||||||
}
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
package org.fdroid.fdroid;
|
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Log;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.IllegalFormatException;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs through all of the translated strings and tests them with the same format
|
|
||||||
* values that the source strings expect. This is to ensure that the formats in
|
|
||||||
* the translations are correct in number and in type (e.g. {@code s} or {@code s}.
|
|
||||||
* It reads the source formats and then builds {@code formats} to represent the
|
|
||||||
* position and type of the formats. Then it runs through all of the translations
|
|
||||||
* with formats of the correct number and type.
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class LocalizationTest {
|
|
||||||
public static final String TAG = "LocalizationTest";
|
|
||||||
|
|
||||||
private final Pattern androidFormat = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)");
|
|
||||||
private final Locale[] locales = Locale.getAvailableLocales();
|
|
||||||
private final HashSet<String> localeNames = new HashSet<>(locales.length);
|
|
||||||
|
|
||||||
private AssetManager assets;
|
|
||||||
private Configuration config;
|
|
||||||
private Resources resources;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
for (Locale locale : Languages.LOCALES_TO_TEST) {
|
|
||||||
localeNames.add(locale.toString());
|
|
||||||
}
|
|
||||||
for (Locale locale : locales) {
|
|
||||||
localeNames.add(locale.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
|
||||||
Context context = instrumentation.getTargetContext();
|
|
||||||
assets = context.getAssets();
|
|
||||||
config = context.getResources().getConfiguration();
|
|
||||||
config.locale = Locale.ENGLISH;
|
|
||||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
|
||||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLoadAllPlural() throws IllegalAccessException {
|
|
||||||
Field[] fields = R.plurals.class.getDeclaredFields();
|
|
||||||
|
|
||||||
HashMap<String, String> haveFormats = new HashMap<>();
|
|
||||||
for (Field field : fields) {
|
|
||||||
//Log.i(TAG, field.getName());
|
|
||||||
int resId = field.getInt(int.class);
|
|
||||||
CharSequence string = resources.getQuantityText(resId, 4);
|
|
||||||
//Log.i(TAG, field.getName() + ": '" + string + "'");
|
|
||||||
Matcher matcher = androidFormat.matcher(string);
|
|
||||||
int matches = 0;
|
|
||||||
char[] formats = new char[5];
|
|
||||||
while (matcher.find()) {
|
|
||||||
String match = matcher.group(0);
|
|
||||||
char formatType = match.charAt(match.length() - 1);
|
|
||||||
switch (match.length()) {
|
|
||||||
case 2:
|
|
||||||
formats[matches] = formatType;
|
|
||||||
matches++;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
haveFormats.put(field.getName(), new String(formats).trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Locale locale : locales) {
|
|
||||||
config.locale = locale;
|
|
||||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
|
||||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
|
||||||
for (Field field : fields) {
|
|
||||||
String formats = null;
|
|
||||||
try {
|
|
||||||
int resId = field.getInt(int.class);
|
|
||||||
for (int quantity = 0; quantity < 567; quantity++) {
|
|
||||||
resources.getQuantityString(resId, quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
formats = haveFormats.get(field.getName());
|
|
||||||
switch (formats) {
|
|
||||||
case "d":
|
|
||||||
resources.getQuantityString(resId, 1, 1);
|
|
||||||
break;
|
|
||||||
case "s":
|
|
||||||
resources.getQuantityString(resId, 1, "ONE");
|
|
||||||
break;
|
|
||||||
case "ds":
|
|
||||||
resources.getQuantityString(resId, 2, 1, "TWO");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (!TextUtils.isEmpty(formats)) {
|
|
||||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IllegalFormatException | Resources.NotFoundException e) {
|
|
||||||
Log.i(TAG, locale + " " + field.getName());
|
|
||||||
throw new IllegalArgumentException("Bad '" + formats + "' format in " + locale + " "
|
|
||||||
+ field.getName() + ": " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLoadAllStrings() throws IllegalAccessException {
|
|
||||||
Field[] fields = R.string.class.getDeclaredFields();
|
|
||||||
|
|
||||||
HashMap<String, String> haveFormats = new HashMap<>();
|
|
||||||
for (Field field : fields) {
|
|
||||||
String string = resources.getString(field.getInt(int.class));
|
|
||||||
Matcher matcher = androidFormat.matcher(string);
|
|
||||||
int matches = 0;
|
|
||||||
char[] formats = new char[5];
|
|
||||||
while (matcher.find()) {
|
|
||||||
String match = matcher.group(0);
|
|
||||||
char formatType = match.charAt(match.length() - 1);
|
|
||||||
switch (match.length()) {
|
|
||||||
case 2:
|
|
||||||
formats[matches] = formatType;
|
|
||||||
matches++;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException(field.getName() + " has bad format: " + match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
haveFormats.put(field.getName(), new String(formats).trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Locale locale : locales) {
|
|
||||||
config.locale = locale;
|
|
||||||
// Resources() requires DisplayMetrics, but they are only needed for drawables
|
|
||||||
resources = new Resources(assets, new DisplayMetrics(), config);
|
|
||||||
for (Field field : fields) {
|
|
||||||
int resId = field.getInt(int.class);
|
|
||||||
resources.getString(resId);
|
|
||||||
|
|
||||||
String formats = haveFormats.get(field.getName());
|
|
||||||
try {
|
|
||||||
switch (formats) {
|
|
||||||
case "d":
|
|
||||||
resources.getString(resId, 1);
|
|
||||||
break;
|
|
||||||
case "dd":
|
|
||||||
resources.getString(resId, 1, 2);
|
|
||||||
break;
|
|
||||||
case "ds":
|
|
||||||
resources.getString(resId, 1, "TWO");
|
|
||||||
break;
|
|
||||||
case "dds":
|
|
||||||
resources.getString(resId, 1, 2, "THREE");
|
|
||||||
break;
|
|
||||||
case "sds":
|
|
||||||
resources.getString(resId, "ONE", 2, "THREE");
|
|
||||||
break;
|
|
||||||
case "s":
|
|
||||||
resources.getString(resId, "ONE");
|
|
||||||
break;
|
|
||||||
case "ss":
|
|
||||||
resources.getString(resId, "ONE", "TWO");
|
|
||||||
break;
|
|
||||||
case "sss":
|
|
||||||
resources.getString(resId, "ONE", "TWO", "THREE");
|
|
||||||
break;
|
|
||||||
case "ssss":
|
|
||||||
resources.getString(resId, "ONE", "TWO", "THREE", "FOUR");
|
|
||||||
break;
|
|
||||||
case "ssd":
|
|
||||||
resources.getString(resId, "ONE", "TWO", 3);
|
|
||||||
break;
|
|
||||||
case "sssd":
|
|
||||||
resources.getString(resId, "ONE", "TWO", "THREE", 4);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (!TextUtils.isEmpty(formats)) {
|
|
||||||
throw new IllegalStateException("Pattern not included in tests: " + formats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.i(TAG, locale + " " + field.getName());
|
|
||||||
throw new IllegalArgumentException("Bad format in '" + locale + "' '" + field.getName() + "': "
|
|
||||||
+ e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
package org.fdroid.fdroid.compat;
|
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.AssetUtils;
|
|
||||||
import org.fdroid.fdroid.data.SanitizedFile;
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assume.assumeTrue;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test needs to run on the emulator, even though it technically could
|
|
||||||
* run as a plain JUnit test, because it is testing the specifics of
|
|
||||||
* Android's symlink handling.
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class FileCompatTest {
|
|
||||||
|
|
||||||
private static final String TAG = "FileCompatTest";
|
|
||||||
|
|
||||||
private SanitizedFile sourceFile;
|
|
||||||
private SanitizedFile destFile;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
|
||||||
File dir = getWriteableDir(instrumentation);
|
|
||||||
sourceFile = SanitizedFile.knownSanitized(
|
|
||||||
AssetUtils.copyAssetToDir(instrumentation.getContext(), "simpleIndex.jar", dir));
|
|
||||||
destFile = new SanitizedFile(dir, "dest-" + UUID.randomUUID() + ".testproduct");
|
|
||||||
assertFalse(destFile.exists());
|
|
||||||
assertTrue(sourceFile.getAbsolutePath() + " should exist.", sourceFile.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void tearDown() {
|
|
||||||
if (!sourceFile.delete()) {
|
|
||||||
Log.w(TAG, "Can't delete " + sourceFile.getAbsolutePath() + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!destFile.delete()) {
|
|
||||||
Log.w(TAG, "Can't delete " + destFile.getAbsolutePath() + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSymlinkRuntime() {
|
|
||||||
FileCompat.symlinkRuntime(sourceFile, destFile);
|
|
||||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSymlinkLibcore() {
|
|
||||||
assumeTrue(Build.VERSION.SDK_INT >= 19);
|
|
||||||
FileCompat.symlinkLibcore(sourceFile, destFile);
|
|
||||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSymlinkOs() {
|
|
||||||
assumeTrue(Build.VERSION.SDK_INT >= 21);
|
|
||||||
FileCompat.symlinkOs(sourceFile, destFile);
|
|
||||||
assertTrue(destFile.getAbsolutePath() + " should exist after symlinking", destFile.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefer internal over external storage, because external tends to be FAT filesystems,
|
|
||||||
* which don't support symlinks (which we test using this method).
|
|
||||||
*/
|
|
||||||
public static File getWriteableDir(Instrumentation instrumentation) {
|
|
||||||
Context context = instrumentation.getContext();
|
|
||||||
Context targetContext = instrumentation.getTargetContext();
|
|
||||||
|
|
||||||
File[] dirsToTry = new File[]{
|
|
||||||
context.getCacheDir(),
|
|
||||||
context.getFilesDir(),
|
|
||||||
targetContext.getCacheDir(),
|
|
||||||
targetContext.getFilesDir(),
|
|
||||||
context.getExternalCacheDir(),
|
|
||||||
context.getExternalFilesDir(null),
|
|
||||||
targetContext.getExternalCacheDir(),
|
|
||||||
targetContext.getExternalFilesDir(null),
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return getWriteableDir(dirsToTry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File getWriteableDir(File[] dirsToTry) {
|
|
||||||
|
|
||||||
for (File dir : dirsToTry) {
|
|
||||||
if (dir != null && dir.canWrite()) {
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,454 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU General Public License
|
|
||||||
* as published by the Free Software Foundation; either version 3
|
|
||||||
* of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.fdroid.fdroid.installer;
|
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import org.fdroid.fdroid.AssetUtils;
|
|
||||||
import org.fdroid.fdroid.Utils;
|
|
||||||
import org.fdroid.fdroid.compat.FileCompatTest;
|
|
||||||
import org.fdroid.fdroid.data.Apk;
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
|
||||||
import org.fdroid.fdroid.data.RepoXMLHandler;
|
|
||||||
import org.fdroid.fdroid.mock.RepoDetails;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
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.HashSet;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
|
|
||||||
* and checking the listed permissions against the ones specified in apks' AndroidManifest,
|
|
||||||
* which have been specifically generated for this test.
|
|
||||||
* <p>
|
|
||||||
* NOTE: This androidTest cannot run as a Robolectric test because the
|
|
||||||
* required methods from PackageManger are not included in Robolectric's Android API.
|
|
||||||
* java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
|
|
||||||
* at android.content.pm.PackageManager.getPackageArchiveInfo(PackageManager.java:3545)
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ApkVerifierTest {
|
|
||||||
public static final String TAG = "ApkVerifierTest";
|
|
||||||
|
|
||||||
Instrumentation instrumentation;
|
|
||||||
|
|
||||||
File sdk14Apk;
|
|
||||||
File minMaxApk;
|
|
||||||
private File extendedPermissionsApk;
|
|
||||||
private File extendedPermsXml;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
instrumentation = InstrumentationRegistry.getInstrumentation();
|
|
||||||
File dir = FileCompatTest.getWriteableDir(instrumentation);
|
|
||||||
assertTrue(dir.isDirectory());
|
|
||||||
assertTrue(dir.canWrite());
|
|
||||||
sdk14Apk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
|
||||||
"org.fdroid.permissions.sdk14.apk",
|
|
||||||
dir
|
|
||||||
);
|
|
||||||
minMaxApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
|
||||||
"org.fdroid.permissions.minmax.apk",
|
|
||||||
dir
|
|
||||||
);
|
|
||||||
extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
|
||||||
"org.fdroid.extendedpermissionstest.apk",
|
|
||||||
dir
|
|
||||||
);
|
|
||||||
extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(),
|
|
||||||
"extendedPerms.xml",
|
|
||||||
dir
|
|
||||||
);
|
|
||||||
assertTrue(sdk14Apk.exists());
|
|
||||||
assertTrue(minMaxApk.exists());
|
|
||||||
assertTrue(extendedPermissionsApk.exists());
|
|
||||||
assertTrue(extendedPermsXml.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testNulls() {
|
|
||||||
assertTrue(ApkVerifier.requestedPermissionsEqual(null, null));
|
|
||||||
|
|
||||||
String[] perms = new String[]{"Blah"};
|
|
||||||
assertFalse(ApkVerifier.requestedPermissionsEqual(perms, null));
|
|
||||||
assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWithoutPrefix() {
|
|
||||||
Apk apk = new Apk();
|
|
||||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
|
||||||
apk.targetSdkVersion = 14;
|
|
||||||
ArrayList<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList(
|
|
||||||
"AUTHENTICATE_ACCOUNTS",
|
|
||||||
"MANAGE_ACCOUNTS",
|
|
||||||
"READ_PROFILE",
|
|
||||||
"WRITE_PROFILE",
|
|
||||||
"GET_ACCOUNTS",
|
|
||||||
"READ_CONTACTS",
|
|
||||||
"WRITE_CONTACTS",
|
|
||||||
"WRITE_EXTERNAL_STORAGE",
|
|
||||||
"READ_EXTERNAL_STORAGE",
|
|
||||||
"INTERNET",
|
|
||||||
"ACCESS_NETWORK_STATE",
|
|
||||||
"NFC",
|
|
||||||
"READ_SYNC_SETTINGS",
|
|
||||||
"WRITE_SYNC_SETTINGS",
|
|
||||||
"WRITE_CALL_LOG", // implied-permission!
|
|
||||||
"READ_CALL_LOG" // implied-permission!
|
|
||||||
));
|
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
|
||||||
noPrefixPermissionsList.add("android.permission.ACCESS_MEDIA_LOCATION");
|
|
||||||
}
|
|
||||||
String[] noPrefixPermissions = noPrefixPermissionsList.toArray(new String[0]);
|
|
||||||
|
|
||||||
for (int i = 0; i < noPrefixPermissions.length; i++) {
|
|
||||||
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
|
|
||||||
}
|
|
||||||
apk.requestedPermissions = noPrefixPermissions;
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(sdk14Apk);
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
|
|
||||||
try {
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
} catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
|
|
||||||
public void testWithMinMax()
|
|
||||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
|
||||||
Apk apk = new Apk();
|
|
||||||
apk.packageName = "org.fdroid.permissions.minmax";
|
|
||||||
apk.targetSdkVersion = 24;
|
|
||||||
ArrayList<String> permissionsList = new ArrayList<>();
|
|
||||||
permissionsList.add("android.permission.READ_CALENDAR");
|
|
||||||
if (Build.VERSION.SDK_INT <= 18) {
|
|
||||||
permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
|
||||||
permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
|
|
||||||
}
|
|
||||||
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(minMaxApk);
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
|
|
||||||
permissionsList.add("ADDITIONAL_PERMISSION");
|
|
||||||
apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWithPrefix() {
|
|
||||||
Apk apk = new Apk();
|
|
||||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
|
||||||
apk.targetSdkVersion = 14;
|
|
||||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
|
||||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
|
||||||
"android.permission.MANAGE_ACCOUNTS",
|
|
||||||
"android.permission.READ_PROFILE",
|
|
||||||
"android.permission.WRITE_PROFILE",
|
|
||||||
"android.permission.GET_ACCOUNTS",
|
|
||||||
"android.permission.READ_CONTACTS",
|
|
||||||
"android.permission.WRITE_CONTACTS",
|
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.READ_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.INTERNET",
|
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
|
||||||
"android.permission.NFC",
|
|
||||||
"android.permission.READ_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
|
||||||
"android.permission.READ_CALL_LOG"// implied-permission!
|
|
||||||
));
|
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
|
||||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
|
||||||
}
|
|
||||||
apk.requestedPermissions = expectedSet.toArray(new String[0]);
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(sdk14Apk);
|
|
||||||
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
|
|
||||||
try {
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
} catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional permissions are okay. The user is simply
|
|
||||||
* warned about a permission that is not used inside the apk
|
|
||||||
*/
|
|
||||||
@Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
|
|
||||||
public void testAdditionalPermission()
|
|
||||||
throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
|
||||||
Apk apk = new Apk();
|
|
||||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
|
||||||
apk.targetSdkVersion = 14;
|
|
||||||
apk.requestedPermissions = new String[]{
|
|
||||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
|
||||||
"android.permission.MANAGE_ACCOUNTS",
|
|
||||||
"android.permission.READ_PROFILE",
|
|
||||||
"android.permission.WRITE_PROFILE",
|
|
||||||
"android.permission.GET_ACCOUNTS",
|
|
||||||
"android.permission.READ_CONTACTS",
|
|
||||||
"android.permission.WRITE_CONTACTS",
|
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.READ_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.INTERNET",
|
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
|
||||||
"android.permission.NFC",
|
|
||||||
"android.permission.READ_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
|
||||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
|
||||||
"android.permission.FAKE_NEW_PERMISSION",
|
|
||||||
};
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(sdk14Apk);
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Missing permissions are not okay!
|
|
||||||
* The user is then not warned about a permission that the apk uses!
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testMissingPermission() {
|
|
||||||
Apk apk = new Apk();
|
|
||||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
|
||||||
apk.targetSdkVersion = 14;
|
|
||||||
apk.requestedPermissions = new String[]{
|
|
||||||
//"android.permission.AUTHENTICATE_ACCOUNTS",
|
|
||||||
"android.permission.MANAGE_ACCOUNTS",
|
|
||||||
"android.permission.READ_PROFILE",
|
|
||||||
"android.permission.WRITE_PROFILE",
|
|
||||||
"android.permission.GET_ACCOUNTS",
|
|
||||||
"android.permission.READ_CONTACTS",
|
|
||||||
"android.permission.WRITE_CONTACTS",
|
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.READ_EXTERNAL_STORAGE",
|
|
||||||
"android.permission.INTERNET",
|
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
|
||||||
"android.permission.NFC",
|
|
||||||
"android.permission.READ_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_CALL_LOG", // implied-permission!
|
|
||||||
"android.permission.READ_CALL_LOG", // implied-permission!
|
|
||||||
};
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(sdk14Apk);
|
|
||||||
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
|
|
||||||
try {
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
fail();
|
|
||||||
} catch (ApkVerifier.ApkVerificationException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
fail(e.getMessage());
|
|
||||||
} catch (ApkVerifier.ApkPermissionUnequalException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExtendedPerms() throws IOException,
|
|
||||||
ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
|
|
||||||
RepoDetails actualDetails = getFromFile(extendedPermsXml);
|
|
||||||
HashSet<String> expectedSet = new HashSet<>(Arrays.asList(
|
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
|
||||||
"android.permission.ACCESS_WIFI_STATE",
|
|
||||||
"android.permission.INTERNET",
|
|
||||||
"android.permission.READ_SYNC_STATS",
|
|
||||||
"android.permission.READ_SYNC_SETTINGS",
|
|
||||||
"android.permission.WRITE_SYNC_SETTINGS",
|
|
||||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
|
||||||
"android.permission.READ_CONTACTS",
|
|
||||||
"android.permission.WRITE_CONTACTS",
|
|
||||||
"android.permission.READ_CALENDAR",
|
|
||||||
"android.permission.WRITE_CALENDAR"
|
|
||||||
));
|
|
||||||
if (Build.VERSION.SDK_INT <= 18) {
|
|
||||||
expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
|
|
||||||
expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT <= 22) {
|
|
||||||
expectedSet.add("android.permission.GET_ACCOUNTS");
|
|
||||||
expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
|
|
||||||
expectedSet.add("android.permission.MANAGE_ACCOUNTS");
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
|
||||||
expectedSet.add("android.permission.CAMERA");
|
|
||||||
if (Build.VERSION.SDK_INT <= 23) {
|
|
||||||
expectedSet.add("android.permission.CALL_PHONE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Apk apk = actualDetails.apks.get(0);
|
|
||||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
|
||||||
for (String permission : expectedSet) {
|
|
||||||
if (!actualSet.contains(permission)) {
|
|
||||||
Log.i(TAG, permission + " in expected but not actual! (android-"
|
|
||||||
+ Build.VERSION.SDK_INT + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (String permission : actualSet) {
|
|
||||||
if (!expectedSet.contains(permission)) {
|
|
||||||
Log.i(TAG, permission + " in actual but not expected! (android-"
|
|
||||||
+ Build.VERSION.SDK_INT + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
|
|
||||||
assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
|
|
||||||
|
|
||||||
String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
|
|
||||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
|
||||||
badPermissions[badPermissions.length - 1] = "notarealpermission";
|
|
||||||
assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
|
|
||||||
|
|
||||||
Uri uri = Uri.fromFile(extendedPermissionsApk);
|
|
||||||
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
|
|
||||||
apkVerifier.verifyApk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testImpliedPerms() 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
|
|
||||||
private RepoDetails getFromFile(File indexFile) throws IOException {
|
|
||||||
InputStream inputStream = null;
|
|
||||||
try {
|
|
||||||
inputStream = new FileInputStream(indexFile);
|
|
||||||
return RepoDetails.getFromFile(inputStream, Repo.PUSH_REQUEST_IGNORE);
|
|
||||||
} finally {
|
|
||||||
Utils.closeQuietly(inputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid.net;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
public class HttpDownloaderTest {
|
|
||||||
private static final String TAG = "HttpDownloaderTest";
|
|
||||||
|
|
||||||
static final String[] URLS;
|
|
||||||
|
|
||||||
// https://developer.android.com/reference/javax/net/ssl/SSLContext
|
|
||||||
static {
|
|
||||||
ArrayList<String> tempUrls = new ArrayList<>(Arrays.asList(
|
|
||||||
"https://f-droid.org/repo/index-v1.jar",
|
|
||||||
// sites that use SNI for HTTPS
|
|
||||||
"https://mirrors.kernel.org/debian/dists/stable/Release",
|
|
||||||
"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;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void downloadUninterruptedTest() throws IOException, InterruptedException {
|
|
||||||
for (String urlString : URLS) {
|
|
||||||
Log.i(TAG, "URL: " + urlString);
|
|
||||||
Uri uri = Uri.parse(urlString);
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
|
||||||
httpDownloader.download();
|
|
||||||
assertTrue(destFile.exists());
|
|
||||||
assertTrue(destFile.canRead());
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void downloadUninterruptedTestWithProgress() throws IOException, InterruptedException {
|
|
||||||
final CountDownLatch latch = new CountDownLatch(1);
|
|
||||||
String urlString = "https://f-droid.org/repo/index.jar";
|
|
||||||
receivedProgress = false;
|
|
||||||
Uri uri = Uri.parse(urlString);
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
|
||||||
httpDownloader.setListener(new ProgressListener() {
|
|
||||||
@Override
|
|
||||||
public void onProgress(long bytesRead, long totalBytes) {
|
|
||||||
receivedProgress = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
httpDownloader.download();
|
|
||||||
latch.countDown();
|
|
||||||
} catch (IOException | InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
|
||||||
assertTrue(destFile.exists());
|
|
||||||
assertTrue(destFile.canRead());
|
|
||||||
assertTrue(receivedProgress);
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void downloadHttpBasicAuth() throws IOException, InterruptedException {
|
|
||||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "myusername", "supersecretpassword");
|
|
||||||
httpDownloader.download();
|
|
||||||
assertTrue(destFile.exists());
|
|
||||||
assertTrue(destFile.canRead());
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void downloadHttpBasicAuthWrongPassword() throws IOException, InterruptedException {
|
|
||||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "myusername", "wrongpassword");
|
|
||||||
httpDownloader.download();
|
|
||||||
assertFalse(destFile.exists());
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void downloadHttpBasicAuthWrongUsername() throws IOException, InterruptedException {
|
|
||||||
Uri uri = Uri.parse("https://httpbin.org/basic-auth/myusername/supersecretpassword");
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
HttpDownloader httpDownloader = new HttpDownloader(uri, destFile, "wrongusername", "supersecretpassword");
|
|
||||||
httpDownloader.download();
|
|
||||||
assertFalse(destFile.exists());
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void downloadThenCancel() throws IOException, InterruptedException {
|
|
||||||
final CountDownLatch latch = new CountDownLatch(2);
|
|
||||||
Uri uri = Uri.parse("https://f-droid.org/repo/index.jar");
|
|
||||||
File destFile = File.createTempFile("dl-", "");
|
|
||||||
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
|
|
||||||
httpDownloader.setListener(new ProgressListener() {
|
|
||||||
@Override
|
|
||||||
public void onProgress(long bytesRead, long totalBytes) {
|
|
||||||
receivedProgress = true;
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
httpDownloader.download();
|
|
||||||
fail();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
fail();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// success!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
latch.await(100, TimeUnit.SECONDS); // either 2 progress reports or 100 seconds
|
|
||||||
httpDownloader.cancelDownload();
|
|
||||||
assertTrue(receivedProgress);
|
|
||||||
destFile.deleteOnExit();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,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);
|
|
||||||
}
|
|
||||||
}
|
|
25
app/src/androidTest/proguard-rules.pro
vendored
25
app/src/androidTest/proguard-rules.pro
vendored
@ -1,25 +0,0 @@
|
|||||||
-dontoptimize
|
|
||||||
-dontwarn
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
-dontwarn android.test.**
|
|
||||||
-dontwarn android.support.test.**
|
|
||||||
-dontnote junit.framework.**
|
|
||||||
-dontnote junit.runner.**
|
|
||||||
|
|
||||||
# Uncomment this if you use Mockito
|
|
||||||
#-dontwarn org.mockito.**
|
|
||||||
|
|
||||||
-keep class org.hamcrest.** { *; }
|
|
||||||
-dontwarn org.hamcrest.**
|
|
||||||
|
|
||||||
-keep class org.junit.** { *; }
|
|
||||||
-dontwarn org.junit.**
|
|
||||||
|
|
||||||
-keep class junit.** { *; }
|
|
||||||
-dontwarn junit.**
|
|
||||||
|
|
||||||
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
|
|
||||||
-keep class androidx.work.multiprocess.RemoteWorkManagerClient {
|
|
||||||
public <init>(...);
|
|
||||||
}
|
|
@ -1,34 +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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy version for basic app flavor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class BluetoothClient {
|
|
||||||
|
|
||||||
public BluetoothClient(String ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public BluetoothConnection openConnection() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,34 +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 android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy version for basic app flavor.
|
|
||||||
*/
|
|
||||||
public class SwapWorkflowActivity {
|
|
||||||
|
|
||||||
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
|
|
||||||
|
|
||||||
public static void requestSwap(Context context, Uri uri) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +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.peers;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy version for basic app flavor.
|
|
||||||
*/
|
|
||||||
public class WifiPeer {
|
|
||||||
public WifiPeer(NewRepoConfig config) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +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.panic;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy version for basic app flavor.
|
|
||||||
*/
|
|
||||||
public class HidingManager {
|
|
||||||
|
|
||||||
public static boolean isHidden(Context context) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showHideDialog(final Context context) {
|
|
||||||
throw new IllegalStateException("unimplemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,99 +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.views.main;
|
|
||||||
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import org.fdroid.fdroid.R;
|
|
||||||
import org.fdroid.fdroid.views.PreferencesFragment;
|
|
||||||
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides which view on the main screen to attach to a given {@link FrameLayout}. This class
|
|
||||||
* doesn't know which view it will be rendering at the time it is constructed. Rather, at some
|
|
||||||
* point in the future the {@link MainViewAdapter} will have information about which view we
|
|
||||||
* are required to render, and will invoke the relevant "bind*()" method on this class.
|
|
||||||
*/
|
|
||||||
class MainViewController extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
private final AppCompatActivity activity;
|
|
||||||
private final FrameLayout frame;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private UpdatesViewBinder updatesView = null;
|
|
||||||
|
|
||||||
MainViewController(AppCompatActivity activity, FrameLayout frame) {
|
|
||||||
super(frame);
|
|
||||||
this.activity = activity;
|
|
||||||
this.frame = frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see LatestViewBinder
|
|
||||||
*/
|
|
||||||
public void bindLatestView() {
|
|
||||||
new LatestViewBinder(activity, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see UpdatesViewBinder
|
|
||||||
*/
|
|
||||||
public void bindUpdates() {
|
|
||||||
if (updatesView == null) {
|
|
||||||
updatesView = new UpdatesViewBinder(activity, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatesView.bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unbindUpdates() {
|
|
||||||
if (updatesView != null) {
|
|
||||||
updatesView.unbind();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void bindCategoriesView() {
|
|
||||||
throw new IllegalStateException("unimplemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void bindSwapView() {
|
|
||||||
throw new IllegalStateException("unimplemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches a {@link PreferencesFragment} to the view. Everything else is managed by the
|
|
||||||
* fragment itself, so no further work needs to be done by this view binder.
|
|
||||||
* <p>
|
|
||||||
* Note: It is tricky to attach a {@link Fragment} to a view from this view holder. This is due
|
|
||||||
* to the way in which the {@link RecyclerView} will reuse existing views and ask us to
|
|
||||||
* put a settings fragment in there at arbitrary times. Usually it wont be the same view we
|
|
||||||
* attached the fragment to last time, which causes weirdness. The solution is to use code from
|
|
||||||
* the com.lsjwzh.widget.recyclerviewpager.FragmentStatePagerAdapter which manages this.
|
|
||||||
* The code has been ported to {@link SettingsView}.
|
|
||||||
*
|
|
||||||
* @see SettingsView
|
|
||||||
*/
|
|
||||||
public void bindSettingsView() {
|
|
||||||
activity.getLayoutInflater().inflate(R.layout.main_tab_settings, frame, true);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
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 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:ignore="MenuTitle">
|
|
||||||
<!-- android:title and android:icon are set dynamically in MainActivity -->
|
|
||||||
<item
|
|
||||||
app:showAsAction="ifRoom|withText"
|
|
||||||
android:id="@+id/latest"/>
|
|
||||||
<item
|
|
||||||
app:showAsAction="ifRoom|withText"
|
|
||||||
android:id="@+id/updates"/>
|
|
||||||
<item
|
|
||||||
app:showAsAction="ifRoom|withText"
|
|
||||||
android:id="@+id/settings"/>
|
|
||||||
</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,183 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<PreferenceScreen android:title="@string/about_title"
|
|
||||||
android:key="pref_about" />
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/preference_category__my_apps">
|
|
||||||
<PreferenceScreen android:title="@string/preference_manage_installed_apps">
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.MAIN"
|
|
||||||
android:targetPackage="@string/applicationId"
|
|
||||||
android:targetClass="org.fdroid.fdroid.views.installed.InstalledAppsActivity"/>
|
|
||||||
</PreferenceScreen>
|
|
||||||
<PreferenceScreen
|
|
||||||
android:title="@string/menu_manage"
|
|
||||||
android:summary="@string/repositories_summary">
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.MAIN"
|
|
||||||
android:targetPackage="@string/applicationId"
|
|
||||||
android:targetClass="org.fdroid.fdroid.views.ManageReposActivity"/>
|
|
||||||
</PreferenceScreen>
|
|
||||||
<PreferenceScreen
|
|
||||||
android:key="installHistory"
|
|
||||||
android:visible="false"
|
|
||||||
android:title="@string/install_history"
|
|
||||||
android:summary="@string/install_history_summary">
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.MAIN"
|
|
||||||
android:targetPackage="@string/applicationId"
|
|
||||||
android:targetClass="org.fdroid.fdroid.views.InstallHistoryActivity"/>
|
|
||||||
</PreferenceScreen>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/updates">
|
|
||||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
|
||||||
android:key="overWifi"
|
|
||||||
android:title="@string/over_wifi"
|
|
||||||
android:defaultValue="@integer/defaultOverWifi"
|
|
||||||
android:layout="@layout/preference_seekbar"/>
|
|
||||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
|
||||||
android:key="overData"
|
|
||||||
android:title="@string/over_data"
|
|
||||||
android:defaultValue="@integer/defaultOverData"
|
|
||||||
android:layout="@layout/preference_seekbar"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/update_auto_download"
|
|
||||||
android:summary="@string/update_auto_download_summary"
|
|
||||||
android:key="updateAutoDownload"/>
|
|
||||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
|
||||||
android:key="updateIntervalSeekBarPosition"
|
|
||||||
android:title="@string/update_interval"
|
|
||||||
android:defaultValue="@integer/defaultUpdateInterval"
|
|
||||||
android:layout="@layout/preference_seekbar"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/notify"
|
|
||||||
android:defaultValue="true"
|
|
||||||
android:key="updateNotify"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/display"
|
|
||||||
android:key="pref_category_display">
|
|
||||||
<ListPreference
|
|
||||||
android:title="@string/pref_language"
|
|
||||||
android:key="language"/>
|
|
||||||
<ListPreference
|
|
||||||
android:title="@string/theme"
|
|
||||||
android:key="theme"
|
|
||||||
android:defaultValue="light"
|
|
||||||
android:entries="@array/themeNames"
|
|
||||||
android:entryValues="@array/themeValues"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/appcompatibility"
|
|
||||||
android:key="pref_category_appcompatibility">
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/show_incompat_versions"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="incompatibleVersions"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/show_anti_feature_apps"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="showAntiFeatureApps"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/force_touch_apps"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="ignoreTouchscreen"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/proxy">
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:key="useTor"
|
|
||||||
android:summary="@string/useTorSummary"
|
|
||||||
android:title="@string/useTor"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="enableProxy"
|
|
||||||
android:title="@string/enable_proxy_title"
|
|
||||||
android:summary="@string/enable_proxy_summary"/>
|
|
||||||
<EditTextPreference
|
|
||||||
android:key="proxyHost"
|
|
||||||
android:title="@string/proxy_host"
|
|
||||||
android:summary="@string/proxy_host_summary"
|
|
||||||
android:dependency="enableProxy"/>
|
|
||||||
<EditTextPreference
|
|
||||||
android:key="proxyPort"
|
|
||||||
android:title="@string/proxy_port"
|
|
||||||
android:summary="@string/proxy_port_summary"
|
|
||||||
android:dependency="enableProxy"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory
|
|
||||||
android:key="pref_category_privacy"
|
|
||||||
android:title="@string/privacy">
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:key="promptToSendCrashReports"
|
|
||||||
android:title="@string/prompt_to_send_crash_reports"
|
|
||||||
android:summary="@string/prompt_to_send_crash_reports_summary"
|
|
||||||
android:defaultValue="true"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="preventScreenshots"
|
|
||||||
android:summary="@string/preventScreenshots_summary"
|
|
||||||
android:title="@string/preventScreenshots_title"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory
|
|
||||||
android:title="@string/other"
|
|
||||||
android:key="pref_category_other">
|
|
||||||
<ListPreference
|
|
||||||
android:title="@string/cache_downloaded"
|
|
||||||
android:key="keepCacheFor"
|
|
||||||
android:defaultValue="86400000"
|
|
||||||
android:entries="@array/keepCacheNames"
|
|
||||||
android:entryValues="@array/keepCacheValues"/>
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:title="@string/expert"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="unstableUpdates"
|
|
||||||
android:title="@string/unstable_updates"
|
|
||||||
android:summary="@string/unstable_updates_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="keepInstallHistory"
|
|
||||||
android:title="@string/keep_install_history"
|
|
||||||
android:summary="@string/keep_install_history_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="sendToFdroidMetrics"
|
|
||||||
android:title="@string/send_to_fdroid_metrics"
|
|
||||||
android:summary="@string/send_to_fdroid_metrics_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="hideAllNotifications"
|
|
||||||
android:title="@string/hide_all_notifications"
|
|
||||||
android:summary="@string/hide_all_notifications_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="sendVersionAndUUIDToServers"
|
|
||||||
android:title="@string/send_version_and_uuid"
|
|
||||||
android:summary="@string/send_version_and_uuid_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="forceOldIndex"
|
|
||||||
android:title="@string/force_old_index"
|
|
||||||
android:summary="@string/force_old_index_summary"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:title="@string/system_installer"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="privilegedInstaller"
|
|
||||||
android:persistent="false"
|
|
||||||
android:dependency="expert"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
</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,179 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
* Copyright (C) 2010-2012 Ciaran Gultnieks
|
|
||||||
* Copyright (C) 2013-2017 Peter Serwylo
|
|
||||||
* Copyright (C) 2014-2015 Daniel Martí
|
|
||||||
* Copyright (C) 2014-2018 Hans-Christoph Steiner
|
|
||||||
* Copyright (C) 2016 Dominik Schürmann
|
|
||||||
* Copyright (C) 2018 Torsten Grote
|
|
||||||
* 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.
|
|
||||||
-->
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="org.fdroid.fdroid"
|
|
||||||
android:installLocation="auto">
|
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.nfc"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.bluetooth"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.usb.host"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.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>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".nearby.SwapWorkflowActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden"
|
|
||||||
android:label="@string/swap"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:parentActivityName=".views.main.MainActivity"
|
|
||||||
android:screenOrientation="portrait">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".views.main.MainActivity" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".panic.PanicPreferencesActivity"
|
|
||||||
android:label="@string/panic_settings"
|
|
||||||
android:parentActivityName=".views.main.MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="info.guardianproject.panic.action.CONNECT" />
|
|
||||||
<action android:name="info.guardianproject.panic.action.DISCONNECT" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".views.main.MainActivity" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".panic.SelectInstalledAppsActivity"
|
|
||||||
android:parentActivityName=".panic.PanicPreferencesActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".panic.PanicResponderActivity"
|
|
||||||
android:noHistory="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
|
|
||||||
<!-- this can never have launchMode singleTask or singleInstance! -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".panic.ExitActivity"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".panic.CalculatorActivity"
|
|
||||||
android:enabled="false"
|
|
||||||
android:icon="@mipmap/ic_calculator_launcher"
|
|
||||||
android:label="@string/hiding_calculator">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<receiver android:name=".nearby.WifiStateChangeReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.net.wifi.STATE_CHANGE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".receiver.DeviceStorageReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver android:name=".nearby.UsbDeviceAttachedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
|
||||||
android:resource="@xml/device_filter" />
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".nearby.UsbDeviceDetachedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
|
|
||||||
android:resource="@xml/device_filter" />
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".nearby.UsbDeviceMediaMountedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MEDIA_EJECT" />
|
|
||||||
<action android:name="android.intent.action.MEDIA_REMOVED" />
|
|
||||||
<action android:name="android.intent.action.MEDIA_MOUNTED" />
|
|
||||||
<action android:name="android.intent.action.MEDIA_BAD_REMOVAL" />
|
|
||||||
|
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:scheme="file" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".nearby.WifiStateChangeService"
|
|
||||||
android:exported="false" />
|
|
||||||
<service android:name=".nearby.SwapService" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".nearby.LocalRepoService"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
|
||||||
android:name=".nearby.TreeUriScannerIntentService"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
|
||||||
android:name=".nearby.SDCardScannerService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
@ -1,123 +0,0 @@
|
|||||||
package javax.jmdns.impl;
|
|
||||||
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.Inet6Address;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
import javax.jmdns.ServiceInfo;
|
|
||||||
import javax.jmdns.impl.util.ByteWrangler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
|
|
||||||
* In order to make it Parcelable (or Serializable for that matter), there are some package-scope
|
|
||||||
* methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
|
|
||||||
* it can access those methods. This is as an alternative to modifying the source code of JmDNS.
|
|
||||||
*/
|
|
||||||
public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
|
|
||||||
|
|
||||||
public FDroidServiceInfo(ServiceInfo info) {
|
|
||||||
super(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the fingerprint of the signing key, or {@code null} if it is not set.
|
|
||||||
*/
|
|
||||||
public String getFingerprint() {
|
|
||||||
// getPropertyString() will return "true" if the value is a zero-length byte array
|
|
||||||
// so we just do a custom version using getPropertyBytes()
|
|
||||||
byte[] data = getPropertyBytes("fingerprint");
|
|
||||||
if (data == null || data.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String fingerprint = ByteWrangler.readUTF(data, 0, data.length);
|
|
||||||
if (TextUtils.isEmpty(fingerprint)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRepoAddress() {
|
|
||||||
return getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readBytes(Parcel in) {
|
|
||||||
byte[] bytes = new byte[in.readInt()];
|
|
||||||
in.readByteArray(bytes);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FDroidServiceInfo(Parcel in) {
|
|
||||||
super(
|
|
||||||
in.readString(),
|
|
||||||
in.readString(),
|
|
||||||
in.readString(),
|
|
||||||
in.readInt(),
|
|
||||||
in.readInt(),
|
|
||||||
in.readInt(),
|
|
||||||
in.readByte() != 0,
|
|
||||||
readBytes(in)
|
|
||||||
);
|
|
||||||
|
|
||||||
int addressCount = in.readInt();
|
|
||||||
for (int i = 0; i < addressCount; i++) {
|
|
||||||
try {
|
|
||||||
addAddress((Inet4Address) Inet4Address.getByAddress(readBytes(in)));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addressCount = in.readInt();
|
|
||||||
for (int i = 0; i < addressCount; i++) {
|
|
||||||
try {
|
|
||||||
addAddress((Inet6Address) Inet6Address.getByAddress(readBytes(in)));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
dest.writeString(getType());
|
|
||||||
dest.writeString(getName());
|
|
||||||
dest.writeString(getSubtype());
|
|
||||||
dest.writeInt(getPort());
|
|
||||||
dest.writeInt(getWeight());
|
|
||||||
dest.writeInt(getPriority());
|
|
||||||
dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
|
|
||||||
dest.writeInt(getTextBytes().length);
|
|
||||||
dest.writeByteArray(getTextBytes());
|
|
||||||
dest.writeInt(getInet4Addresses().length);
|
|
||||||
for (int i = 0; i < getInet4Addresses().length; i++) {
|
|
||||||
Inet4Address address = getInet4Addresses()[i];
|
|
||||||
dest.writeInt(address.getAddress().length);
|
|
||||||
dest.writeByteArray(address.getAddress());
|
|
||||||
}
|
|
||||||
dest.writeInt(getInet6Addresses().length);
|
|
||||||
for (int i = 0; i < getInet6Addresses().length; i++) {
|
|
||||||
Inet6Address address = getInet6Addresses()[i];
|
|
||||||
dest.writeInt(address.getAddress().length);
|
|
||||||
dest.writeByteArray(address.getAddress());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Parcelable.Creator<FDroidServiceInfo> CREATOR = new Parcelable.Creator<FDroidServiceInfo>() {
|
|
||||||
public FDroidServiceInfo createFromParcel(Parcel source) {
|
|
||||||
return new FDroidServiceInfo(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FDroidServiceInfo[] newArray(int size) {
|
|
||||||
return new FDroidServiceInfo[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public abstract class AbstractLogger implements LoggerInterface {
|
|
||||||
|
|
||||||
protected String category;
|
|
||||||
|
|
||||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH);
|
|
||||||
|
|
||||||
public AbstractLogger(String category) {
|
|
||||||
this.category = category;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String format(String level, String message) {
|
|
||||||
return String.format("%s %s %s: %s\n", dateFormat.format(new Date()), level, category, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void write(String level, String message, Throwable t);
|
|
||||||
|
|
||||||
protected void writeFixNullMessage(String level, String message, Throwable t) {
|
|
||||||
if (message == null) {
|
|
||||||
if (t != null) message = t.getClass().getName();
|
|
||||||
else message = "null";
|
|
||||||
}
|
|
||||||
write(level, message, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void debug(String message, Throwable t) {
|
|
||||||
writeFixNullMessage(DEBUG, message, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void debug(String message) {
|
|
||||||
writeFixNullMessage(DEBUG, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void error(String message, Throwable t) {
|
|
||||||
writeFixNullMessage(ERROR, message, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void error(String message) {
|
|
||||||
writeFixNullMessage(ERROR, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void info(String message, Throwable t) {
|
|
||||||
writeFixNullMessage(INFO, message, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void info(String message) {
|
|
||||||
writeFixNullMessage(INFO, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void warning(String message, Throwable t) {
|
|
||||||
writeFixNullMessage(WARNING, message, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void warning(String message) {
|
|
||||||
writeFixNullMessage(WARNING, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDebugEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isErrorEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInfoEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isWarningEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
public class ConsoleLoggerFactory implements LoggerFactory {
|
|
||||||
|
|
||||||
public LoggerInterface getLogger(String category) {
|
|
||||||
return new StreamLogger(category, System.out);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
public interface LoggerFactory {
|
|
||||||
|
|
||||||
public LoggerInterface getLogger(String category);
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
public interface LoggerInterface {
|
|
||||||
|
|
||||||
public static final String ERROR = "ERROR";
|
|
||||||
public static final String WARNING = "WARNING";
|
|
||||||
public static final String INFO = "INFO";
|
|
||||||
public static final String DEBUG = "DEBUG";
|
|
||||||
|
|
||||||
public boolean isErrorEnabled();
|
|
||||||
|
|
||||||
public void error(String message);
|
|
||||||
|
|
||||||
public void error(String message, Throwable t);
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isWarningEnabled();
|
|
||||||
|
|
||||||
public void warning(String message);
|
|
||||||
|
|
||||||
public void warning(String message, Throwable t);
|
|
||||||
|
|
||||||
public boolean isInfoEnabled();
|
|
||||||
|
|
||||||
public void info(String message);
|
|
||||||
|
|
||||||
public void info(String message, Throwable t);
|
|
||||||
|
|
||||||
public boolean isDebugEnabled();
|
|
||||||
|
|
||||||
public void debug(String message);
|
|
||||||
|
|
||||||
public void debug(String message, Throwable t);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
public class LoggerManager {
|
|
||||||
|
|
||||||
static LoggerFactory factory = new NullLoggerFactory();
|
|
||||||
|
|
||||||
static Map<String, LoggerInterface> loggers = new TreeMap<String, LoggerInterface>();
|
|
||||||
|
|
||||||
public static void setLoggerFactory(LoggerFactory f) {
|
|
||||||
factory = f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LoggerInterface getLogger(String category) {
|
|
||||||
|
|
||||||
LoggerInterface logger = loggers.get(category);
|
|
||||||
if (logger == null) {
|
|
||||||
logger = factory.getLogger(category);
|
|
||||||
loggers.put(category, logger);
|
|
||||||
}
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
public class NullLoggerFactory implements LoggerFactory {
|
|
||||||
|
|
||||||
static LoggerInterface logger = new LoggerInterface() {
|
|
||||||
|
|
||||||
public void debug(String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void debug(String message, Throwable t) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void error(String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void error(String message, Throwable t) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void info(String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void info(String message, Throwable t) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDebugEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isErrorEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInfoEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isWarningEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void warning(String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void warning(String message, Throwable t) {
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
public LoggerInterface getLogger(String category) {
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.logging;
|
|
||||||
|
|
||||||
import java.io.PrintStream;
|
|
||||||
|
|
||||||
public class StreamLogger extends AbstractLogger {
|
|
||||||
|
|
||||||
PrintStream out;
|
|
||||||
|
|
||||||
public StreamLogger(String category, PrintStream out) {
|
|
||||||
super(category);
|
|
||||||
this.out = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void write(String level, String message, Throwable t) {
|
|
||||||
out.print(format(level, message));
|
|
||||||
if (t != null) t.printStackTrace(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
public class AutoKeyException extends RuntimeException {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
public AutoKeyException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AutoKeyException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This class provides Base64 encoding services using one of several possible
|
|
||||||
* implementations available elsewhere in the classpath. Supported implementations
|
|
||||||
* are android.util.Base64 and org.bouncycastle.util.encoders.Base64Encoder.
|
|
||||||
* These APIs are accessed via reflection, and as long as at least one is available
|
|
||||||
* Base64 encoding is possible. This technique provides compatibility across different
|
|
||||||
* Android OS versions, and also allows zipsigner-lib to operate in desktop environments
|
|
||||||
* as long as the BouncyCastle provider jar is in the classpath.
|
|
||||||
*
|
|
||||||
* android.util.Base64 was added in API level 8 (Android 2.2, Froyo)
|
|
||||||
* org.bouncycastle.util.encoders.Base64Encoder was removed in API level 11 (Android 3.0, Honeycomb)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public class Base64 {
|
|
||||||
|
|
||||||
static Method aEncodeMethod = null; // Reference to the android.util.Base64.encode() method, if available
|
|
||||||
static Method aDecodeMethod = null; // Reference to the android.util.Base64.decode() method, if available
|
|
||||||
|
|
||||||
static Object bEncoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
|
|
||||||
static Method bEncodeMethod = null; // Reference to the bEncoder.encode() method, if available
|
|
||||||
|
|
||||||
static Object bDecoder = null; // Reference to an org.bouncycastle.util.encoders.Base64Encoder instance, if available
|
|
||||||
static Method bDecodeMethod = null; // Reference to the bEncoder.encode() method, if available
|
|
||||||
|
|
||||||
static LoggerInterface logger = null;
|
|
||||||
|
|
||||||
static {
|
|
||||||
|
|
||||||
Class<Object> clazz;
|
|
||||||
|
|
||||||
logger = LoggerManager.getLogger(Base64.class.getName());
|
|
||||||
|
|
||||||
try {
|
|
||||||
clazz = (Class<Object>) Class.forName("android.util.Base64");
|
|
||||||
// Looking for encode( byte[] input, int flags)
|
|
||||||
aEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE);
|
|
||||||
aDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE);
|
|
||||||
logger.info(clazz.getName() + " is available.");
|
|
||||||
} catch (ClassNotFoundException x) {
|
|
||||||
} // Ignore
|
|
||||||
catch (Exception x) {
|
|
||||||
logger.error("Failed to initialize use of android.util.Base64", x);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
clazz = (Class<Object>) Class.forName("org.bouncycastle.util.encoders.Base64Encoder");
|
|
||||||
bEncoder = clazz.newInstance();
|
|
||||||
// Looking for encode( byte[] input, int offset, int length, OutputStream output)
|
|
||||||
bEncodeMethod = clazz.getMethod("encode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
|
|
||||||
logger.info(clazz.getName() + " is available.");
|
|
||||||
// Looking for decode( byte[] input, int offset, int length, OutputStream output)
|
|
||||||
bDecodeMethod = clazz.getMethod("decode", byte[].class, Integer.TYPE, Integer.TYPE, OutputStream.class);
|
|
||||||
|
|
||||||
} catch (ClassNotFoundException x) {
|
|
||||||
} // Ignore
|
|
||||||
catch (Exception x) {
|
|
||||||
logger.error("Failed to initialize use of org.bouncycastle.util.encoders.Base64Encoder", x);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aEncodeMethod == null && bEncodeMethod == null)
|
|
||||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static String encode(byte[] data) {
|
|
||||||
try {
|
|
||||||
if (aEncodeMethod != null) {
|
|
||||||
// Invoking a static method call, using null for the instance value
|
|
||||||
byte[] encodedBytes = (byte[]) aEncodeMethod.invoke(null, data, 2);
|
|
||||||
return new String(encodedBytes);
|
|
||||||
} else if (bEncodeMethod != null) {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
bEncodeMethod.invoke(bEncoder, data, 0, data.length, baos);
|
|
||||||
return new String(baos.toByteArray());
|
|
||||||
}
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] decode(byte[] data) {
|
|
||||||
try {
|
|
||||||
if (aDecodeMethod != null) {
|
|
||||||
// Invoking a static method call, using null for the instance value
|
|
||||||
byte[] decodedBytes = (byte[]) aDecodeMethod.invoke(null, data, 2);
|
|
||||||
return decodedBytes;
|
|
||||||
} else if (bDecodeMethod != null) {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
bDecodeMethod.invoke(bEncoder, data, 0, data.length, baos);
|
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
throw new IllegalStateException("No base64 encoder implementation is available.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default resource adapter.
|
|
||||||
*/
|
|
||||||
public class DefaultResourceAdapter implements ResourceAdapter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getString(Item item, Object... args) {
|
|
||||||
|
|
||||||
switch (item) {
|
|
||||||
case INPUT_SAME_AS_OUTPUT_ERROR:
|
|
||||||
return "Input and output files are the same. Specify a different name for the output.";
|
|
||||||
case AUTO_KEY_SELECTION_ERROR:
|
|
||||||
return "Unable to auto-select key for signing " + args[0];
|
|
||||||
case LOADING_CERTIFICATE_AND_KEY:
|
|
||||||
return "Loading certificate and private key";
|
|
||||||
case PARSING_CENTRAL_DIRECTORY:
|
|
||||||
return "Parsing the input's central directory";
|
|
||||||
case GENERATING_MANIFEST:
|
|
||||||
return "Generating manifest";
|
|
||||||
case GENERATING_SIGNATURE_FILE:
|
|
||||||
return "Generating signature file";
|
|
||||||
case GENERATING_SIGNATURE_BLOCK:
|
|
||||||
return "Generating signature block file";
|
|
||||||
case COPYING_ZIP_ENTRY:
|
|
||||||
return String.format(Locale.ENGLISH, "Copying zip entry %d of %d", args[0], args[1]);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown item " + item);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Produces the classic hex dump with an address column, hex data
|
|
||||||
* section (16 bytes per row) and right-column printable character dislpay.
|
|
||||||
*/
|
|
||||||
public class HexDumpEncoder {
|
|
||||||
|
|
||||||
static HexEncoder encoder = new HexEncoder();
|
|
||||||
|
|
||||||
public static String encode(byte[] data) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
encoder.encode(data, 0, data.length, baos);
|
|
||||||
byte[] hex = baos.toByteArray();
|
|
||||||
|
|
||||||
StringBuilder hexDumpOut = new StringBuilder();
|
|
||||||
for (int i = 0; i < hex.length; i += 32) {
|
|
||||||
|
|
||||||
int max = Math.min(i + 32, hex.length);
|
|
||||||
|
|
||||||
StringBuilder hexOut = new StringBuilder();
|
|
||||||
StringBuilder chrOut = new StringBuilder();
|
|
||||||
|
|
||||||
hexOut.append(String.format("%08x: ", (i / 2)));
|
|
||||||
|
|
||||||
for (int j = i; j < max; j += 2) {
|
|
||||||
hexOut.append(Character.valueOf((char) hex[j]));
|
|
||||||
hexOut.append(Character.valueOf((char) hex[j + 1]));
|
|
||||||
if ((j + 2) % 4 == 0) hexOut.append(' ');
|
|
||||||
|
|
||||||
int dataChar = data[j / 2];
|
|
||||||
if (dataChar >= 32 && dataChar < 127) chrOut.append(Character.valueOf((char) dataChar));
|
|
||||||
else chrOut.append('.');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hexDumpOut.append(hexOut.toString());
|
|
||||||
for (int k = hexOut.length(); k < 50; k++) hexDumpOut.append(' ');
|
|
||||||
hexDumpOut.append(" ");
|
|
||||||
hexDumpOut.append(chrOut);
|
|
||||||
hexDumpOut.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return hexDumpOut.toString();
|
|
||||||
} catch (IOException x) {
|
|
||||||
throw new IllegalStateException(x.getClass().getName() + ": " + x.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,182 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
/*
|
|
||||||
This file is a copy of org.bouncycastle.util.encoders.HexEncoder.
|
|
||||||
|
|
||||||
Please note: our license is an adaptation of the MIT X11 License and should be read as such.
|
|
||||||
License
|
|
||||||
|
|
||||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class HexEncoder {
|
|
||||||
protected final byte[] encodingTable =
|
|
||||||
{
|
|
||||||
(byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
|
|
||||||
(byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f'
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* set up the decoding table.
|
|
||||||
*/
|
|
||||||
protected final byte[] decodingTable = new byte[128];
|
|
||||||
|
|
||||||
protected void initialiseDecodingTable() {
|
|
||||||
for (int i = 0; i < encodingTable.length; i++) {
|
|
||||||
decodingTable[encodingTable[i]] = (byte) i;
|
|
||||||
}
|
|
||||||
|
|
||||||
decodingTable['A'] = decodingTable['a'];
|
|
||||||
decodingTable['B'] = decodingTable['b'];
|
|
||||||
decodingTable['C'] = decodingTable['c'];
|
|
||||||
decodingTable['D'] = decodingTable['d'];
|
|
||||||
decodingTable['E'] = decodingTable['e'];
|
|
||||||
decodingTable['F'] = decodingTable['f'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public HexEncoder() {
|
|
||||||
initialiseDecodingTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* encode the input data producing a Hex output stream.
|
|
||||||
*
|
|
||||||
* @return the number of bytes produced.
|
|
||||||
*/
|
|
||||||
public int encode(
|
|
||||||
byte[] data,
|
|
||||||
int off,
|
|
||||||
int length,
|
|
||||||
OutputStream out)
|
|
||||||
throws IOException {
|
|
||||||
for (int i = off; i < (off + length); i++) {
|
|
||||||
int v = data[i] & 0xff;
|
|
||||||
|
|
||||||
out.write(encodingTable[(v >>> 4)]);
|
|
||||||
out.write(encodingTable[v & 0xf]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return length * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean ignore(
|
|
||||||
char c) {
|
|
||||||
return (c == '\n' || c == '\r' || c == '\t' || c == ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* decode the Hex encoded byte data writing it to the given output stream,
|
|
||||||
* whitespace characters will be ignored.
|
|
||||||
*
|
|
||||||
* @return the number of bytes produced.
|
|
||||||
*/
|
|
||||||
public int decode(
|
|
||||||
byte[] data,
|
|
||||||
int off,
|
|
||||||
int length,
|
|
||||||
OutputStream out)
|
|
||||||
throws IOException {
|
|
||||||
byte b1, b2;
|
|
||||||
int outLen = 0;
|
|
||||||
|
|
||||||
int end = off + length;
|
|
||||||
|
|
||||||
while (end > off) {
|
|
||||||
if (!ignore((char) data[end - 1])) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
end--;
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = off;
|
|
||||||
while (i < end) {
|
|
||||||
while (i < end && ignore((char) data[i])) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
b1 = decodingTable[data[i++]];
|
|
||||||
|
|
||||||
while (i < end && ignore((char) data[i])) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
b2 = decodingTable[data[i++]];
|
|
||||||
|
|
||||||
out.write((b1 << 4) | b2);
|
|
||||||
|
|
||||||
outLen++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return outLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* decode the Hex encoded String data writing it to the given output stream,
|
|
||||||
* whitespace characters will be ignored.
|
|
||||||
*
|
|
||||||
* @return the number of bytes produced.
|
|
||||||
*/
|
|
||||||
public int decode(
|
|
||||||
String data,
|
|
||||||
OutputStream out)
|
|
||||||
throws IOException {
|
|
||||||
byte b1, b2;
|
|
||||||
int length = 0;
|
|
||||||
|
|
||||||
int end = data.length();
|
|
||||||
|
|
||||||
while (end > 0) {
|
|
||||||
if (!ignore(data.charAt(end - 1))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
end--;
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
while (i < end) {
|
|
||||||
while (i < end && ignore(data.charAt(i))) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
b1 = decodingTable[data.charAt(i++)];
|
|
||||||
|
|
||||||
while (i < end && ignore(data.charAt(i))) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
b2 = decodingTable[data.charAt(i++)];
|
|
||||||
|
|
||||||
out.write((b1 << 4) | b2);
|
|
||||||
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
public class KeySet {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
|
|
||||||
// certificate
|
|
||||||
X509Certificate publicKey = null;
|
|
||||||
|
|
||||||
// private key
|
|
||||||
PrivateKey privateKey = null;
|
|
||||||
|
|
||||||
// signature block template
|
|
||||||
byte[] sigBlockTemplate = null;
|
|
||||||
|
|
||||||
String signatureAlgorithm = "SHA1withRSA";
|
|
||||||
|
|
||||||
public KeySet() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] sigBlockTemplate) {
|
|
||||||
this.name = name;
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
this.privateKey = privateKey;
|
|
||||||
this.sigBlockTemplate = sigBlockTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeySet(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] sigBlockTemplate) {
|
|
||||||
this.name = name;
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
this.privateKey = privateKey;
|
|
||||||
if (signatureAlgorithm != null) this.signatureAlgorithm = signatureAlgorithm;
|
|
||||||
this.sigBlockTemplate = sigBlockTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getPublicKey() {
|
|
||||||
return publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPublicKey(X509Certificate publicKey) {
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrivateKey getPrivateKey() {
|
|
||||||
return privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrivateKey(PrivateKey privateKey) {
|
|
||||||
this.privateKey = privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getSigBlockTemplate() {
|
|
||||||
return sigBlockTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSigBlockTemplate(byte[] sigBlockTemplate) {
|
|
||||||
this.sigBlockTemplate = sigBlockTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSignatureAlgorithm() {
|
|
||||||
return signatureAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignatureAlgorithm(String signatureAlgorithm) {
|
|
||||||
if (signatureAlgorithm == null) signatureAlgorithm = "SHA1withRSA";
|
|
||||||
else this.signatureAlgorithm = signatureAlgorithm;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
public class ProgressEvent {
|
|
||||||
|
|
||||||
public static final int PRORITY_NORMAL = 0;
|
|
||||||
public static final int PRORITY_IMPORTANT = 1;
|
|
||||||
|
|
||||||
private String message;
|
|
||||||
private int percentDone;
|
|
||||||
private int priority;
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPercentDone() {
|
|
||||||
return percentDone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPercentDone(int percentDone) {
|
|
||||||
this.percentDone = percentDone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPriority() {
|
|
||||||
return priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPriority(int priority) {
|
|
||||||
this.priority = priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class ProgressHelper {
|
|
||||||
|
|
||||||
private int progressTotalItems = 0;
|
|
||||||
private int progressCurrentItem = 0;
|
|
||||||
private ProgressEvent progressEvent = new ProgressEvent();
|
|
||||||
|
|
||||||
public void initProgress() {
|
|
||||||
progressTotalItems = 10000;
|
|
||||||
progressCurrentItem = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getProgressTotalItems() {
|
|
||||||
return progressTotalItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgressTotalItems(int progressTotalItems) {
|
|
||||||
this.progressTotalItems = progressTotalItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getProgressCurrentItem() {
|
|
||||||
return progressCurrentItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgressCurrentItem(int progressCurrentItem) {
|
|
||||||
this.progressCurrentItem = progressCurrentItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void progress(int priority, String message) {
|
|
||||||
|
|
||||||
progressCurrentItem += 1;
|
|
||||||
|
|
||||||
int percentDone;
|
|
||||||
if (progressTotalItems == 0) percentDone = 0;
|
|
||||||
else percentDone = (100 * progressCurrentItem) / progressTotalItems;
|
|
||||||
|
|
||||||
// Notify listeners here
|
|
||||||
for (ProgressListener listener : listeners) {
|
|
||||||
progressEvent.setMessage(message);
|
|
||||||
progressEvent.setPercentDone(percentDone);
|
|
||||||
progressEvent.setPriority(priority);
|
|
||||||
listener.onProgress(progressEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<ProgressListener> listeners = new ArrayList<ProgressListener>();
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public synchronized void addProgressListener(ProgressListener l) {
|
|
||||||
ArrayList<ProgressListener> list = (ArrayList<ProgressListener>) listeners.clone();
|
|
||||||
list.add(l);
|
|
||||||
listeners = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public synchronized void removeProgressListener(ProgressListener l) {
|
|
||||||
ArrayList<ProgressListener> list = (ArrayList<ProgressListener>) listeners.clone();
|
|
||||||
list.remove(l);
|
|
||||||
listeners = list;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
public interface ProgressListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to notify the listener that progress has been made during
|
|
||||||
* the zip signing operation.
|
|
||||||
*/
|
|
||||||
public void onProgress(ProgressEvent event);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to obtain internationalized strings for the progress events.
|
|
||||||
*/
|
|
||||||
public interface ResourceAdapter {
|
|
||||||
|
|
||||||
public enum Item {
|
|
||||||
INPUT_SAME_AS_OUTPUT_ERROR,
|
|
||||||
AUTO_KEY_SELECTION_ERROR,
|
|
||||||
LOADING_CERTIFICATE_AND_KEY,
|
|
||||||
PARSING_CENTRAL_DIRECTORY,
|
|
||||||
GENERATING_MANIFEST,
|
|
||||||
GENERATING_SIGNATURE_FILE,
|
|
||||||
GENERATING_SIGNATURE_BLOCK,
|
|
||||||
COPYING_ZIP_ENTRY
|
|
||||||
}
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
public String getString(Item item, Object... args);
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood.
|
|
||||||
*
|
|
||||||
* 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 kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
public class ZipSignature {
|
|
||||||
|
|
||||||
byte[] beforeAlgorithmIdBytes = {0x30, 0x21};
|
|
||||||
|
|
||||||
// byte[] algorithmIdBytes;
|
|
||||||
// algorithmIdBytes = sun.security.x509.AlgorithmId.get("SHA1").encode();
|
|
||||||
byte[] algorithmIdBytes = {0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00};
|
|
||||||
|
|
||||||
byte[] afterAlgorithmIdBytes = {0x04, 0x14};
|
|
||||||
|
|
||||||
Cipher cipher;
|
|
||||||
|
|
||||||
MessageDigest md;
|
|
||||||
|
|
||||||
|
|
||||||
public ZipSignature() throws IOException, GeneralSecurityException {
|
|
||||||
md = MessageDigest.getInstance("SHA1");
|
|
||||||
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initSign(PrivateKey privateKey) throws InvalidKeyException {
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(byte[] data) {
|
|
||||||
md.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(byte[] data, int offset, int count) {
|
|
||||||
md.update(data, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] sign() throws BadPaddingException, IllegalBlockSizeException {
|
|
||||||
cipher.update(beforeAlgorithmIdBytes);
|
|
||||||
cipher.update(algorithmIdBytes);
|
|
||||||
cipher.update(afterAlgorithmIdBytes);
|
|
||||||
cipher.update(md.digest());
|
|
||||||
return cipher.doFinal();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,796 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
* Copyright (C) 2008 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* This file is a heavily modified version of com.android.signapk.SignApk.java.
|
|
||||||
* The changes include:
|
|
||||||
* - addition of the signZip() convenience methods
|
|
||||||
* - addition of a progress listener interface
|
|
||||||
* - removal of main()
|
|
||||||
* - switch to a signature generation method that verifies
|
|
||||||
* in Android recovery
|
|
||||||
* - eliminated dependency on sun.security and sun.misc APIs by
|
|
||||||
* using signature block template files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package kellinwood.security.zipsigner;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
import kellinwood.zipio.ZioEntry;
|
|
||||||
import kellinwood.zipio.ZipInput;
|
|
||||||
import kellinwood.zipio.ZipOutput;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.security.DigestOutputStream;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.KeySpec;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Observable;
|
|
||||||
import java.util.Observer;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a modified copy of com.android.signapk.SignApk.java. It provides an
|
|
||||||
* API to sign JAR files (including APKs and Zip/OTA updates) in
|
|
||||||
* a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
|
|
||||||
* <p>
|
|
||||||
* Please see the README.txt file in the root of this project for usage instructions.
|
|
||||||
*/
|
|
||||||
public class ZipSigner {
|
|
||||||
|
|
||||||
private boolean canceled = false;
|
|
||||||
|
|
||||||
private ProgressHelper progressHelper = new ProgressHelper();
|
|
||||||
private ResourceAdapter resourceAdapter = new DefaultResourceAdapter();
|
|
||||||
|
|
||||||
static LoggerInterface log = null;
|
|
||||||
|
|
||||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
|
||||||
private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
|
|
||||||
|
|
||||||
// Files matching this pattern are not copied to the output.
|
|
||||||
private static Pattern stripPattern =
|
|
||||||
Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
|
|
||||||
|
|
||||||
Map<String, KeySet> loadedKeys = new HashMap<String, KeySet>();
|
|
||||||
KeySet keySet = null;
|
|
||||||
|
|
||||||
public static LoggerInterface getLogger() {
|
|
||||||
if (log == null) log = LoggerManager.getLogger(ZipSigner.class.getName());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String MODE_AUTO_TESTKEY = "auto-testkey";
|
|
||||||
public static final String MODE_AUTO_NONE = "auto-none";
|
|
||||||
public static final String MODE_AUTO = "auto";
|
|
||||||
public static final String KEY_NONE = "none";
|
|
||||||
public static final String KEY_TESTKEY = "testkey";
|
|
||||||
|
|
||||||
// Allowable key modes.
|
|
||||||
public static final String[] SUPPORTED_KEY_MODES =
|
|
||||||
new String[]{MODE_AUTO_TESTKEY, MODE_AUTO, MODE_AUTO_NONE, "media", "platform", "shared", KEY_TESTKEY, KEY_NONE};
|
|
||||||
|
|
||||||
String keymode = KEY_TESTKEY; // backwards compatible with versions that only signed with this key
|
|
||||||
|
|
||||||
Map<String, String> autoKeyDetect = new HashMap<String, String>();
|
|
||||||
|
|
||||||
AutoKeyObservable autoKeyObservable = new AutoKeyObservable();
|
|
||||||
|
|
||||||
public ZipSigner() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
|
||||||
// MD5 of the first 1458 bytes of the signature block generated by the key, mapped to the key name
|
|
||||||
autoKeyDetect.put("aa9852bc5a53272ac8031d49b65e4b0e", "media");
|
|
||||||
autoKeyDetect.put("e60418c4b638f20d0721e115674ca11f", "platform");
|
|
||||||
autoKeyDetect.put("3e24e49741b60c215c010dc6048fca7d", "shared");
|
|
||||||
autoKeyDetect.put("dab2cead827ef5313f28e22b6fa8479f", "testkey");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResourceAdapter getResourceAdapter() {
|
|
||||||
return resourceAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResourceAdapter(ResourceAdapter resourceAdapter) {
|
|
||||||
this.resourceAdapter = resourceAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the key mode is automatic, the observers are called when the key is determined
|
|
||||||
public void addAutoKeyObserver(Observer o) {
|
|
||||||
autoKeyObservable.addObserver(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKeymode() {
|
|
||||||
return keymode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeymode(String km) throws IOException, GeneralSecurityException {
|
|
||||||
if (getLogger().isDebugEnabled()) getLogger().debug("setKeymode: " + km);
|
|
||||||
keymode = km;
|
|
||||||
if (keymode.startsWith(MODE_AUTO)) {
|
|
||||||
keySet = null;
|
|
||||||
} else {
|
|
||||||
progressHelper.initProgress();
|
|
||||||
loadKeys(keymode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] getSupportedKeyModes() {
|
|
||||||
return SUPPORTED_KEY_MODES;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected String autoDetectKey(String mode, Map<String, ZioEntry> zioEntries)
|
|
||||||
throws NoSuchAlgorithmException, IOException {
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
if (!mode.startsWith(MODE_AUTO)) return mode;
|
|
||||||
|
|
||||||
|
|
||||||
// Auto-determine which keys to use
|
|
||||||
String keyName = null;
|
|
||||||
// Start by finding the signature block file in the input.
|
|
||||||
for (Map.Entry<String, ZioEntry> entry : zioEntries.entrySet()) {
|
|
||||||
String entryName = entry.getKey();
|
|
||||||
if (entryName.startsWith("META-INF/") && entryName.endsWith(".RSA")) {
|
|
||||||
|
|
||||||
// Compute MD5 of the first 1458 bytes, which is the size of our signature block templates --
|
|
||||||
// e.g., the portion of the sig block file that is the same for a given certificate.
|
|
||||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
|
||||||
byte[] entryData = entry.getValue().getData();
|
|
||||||
if (entryData.length < 1458) break; // sig block too short to be a supported key
|
|
||||||
md5.update(entryData, 0, 1458);
|
|
||||||
byte[] rawDigest = md5.digest();
|
|
||||||
|
|
||||||
// Create the hex representation of the digest value
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (byte b : rawDigest) {
|
|
||||||
builder.append(String.format("%02x", b));
|
|
||||||
}
|
|
||||||
|
|
||||||
String md5String = builder.toString();
|
|
||||||
// Lookup the key name
|
|
||||||
keyName = autoKeyDetect.get(md5String);
|
|
||||||
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
if (keyName != null) {
|
|
||||||
getLogger().debug(String.format("Auto-determined key=%s using md5=%s", keyName, md5String));
|
|
||||||
} else {
|
|
||||||
getLogger().debug(String.format("Auto key determination failed for md5=%s", md5String));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (keyName != null) return keyName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode.equals(MODE_AUTO_TESTKEY)) {
|
|
||||||
// in auto-testkey mode, fallback to the testkey if it couldn't be determined
|
|
||||||
if (debug) getLogger().debug("Falling back to key=" + keyName);
|
|
||||||
return KEY_TESTKEY;
|
|
||||||
|
|
||||||
} else if (mode.equals(MODE_AUTO_NONE)) {
|
|
||||||
// in auto-node mode, simply copy the input to the output when the key can't be determined.
|
|
||||||
if (debug) getLogger().debug("Unable to determine key, returning: " + KEY_NONE);
|
|
||||||
return KEY_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void issueLoadingCertAndKeysProgressEvent() {
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.LOADING_CERTIFICATE_AND_KEY));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads one of the built-in keys (media, platform, shared, testkey)
|
|
||||||
public void loadKeys(String name)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
|
|
||||||
keySet = loadedKeys.get(name);
|
|
||||||
if (keySet != null) return;
|
|
||||||
|
|
||||||
keySet = new KeySet();
|
|
||||||
keySet.setName(name);
|
|
||||||
loadedKeys.put(name, keySet);
|
|
||||||
|
|
||||||
if (KEY_NONE.equals(name)) return;
|
|
||||||
|
|
||||||
issueLoadingCertAndKeysProgressEvent();
|
|
||||||
|
|
||||||
// load the private key
|
|
||||||
URL privateKeyUrl = getClass().getResource("/keys/" + name + ".pk8");
|
|
||||||
keySet.setPrivateKey(readPrivateKey(privateKeyUrl, null));
|
|
||||||
|
|
||||||
// load the certificate
|
|
||||||
URL publicKeyUrl = getClass().getResource("/keys/" + name + ".x509.pem");
|
|
||||||
keySet.setPublicKey(readPublicKey(publicKeyUrl));
|
|
||||||
|
|
||||||
// load the signature block template
|
|
||||||
URL sigBlockTemplateUrl = getClass().getResource("/keys/" + name + ".sbt");
|
|
||||||
if (sigBlockTemplateUrl != null) {
|
|
||||||
keySet.setSigBlockTemplate(readContentAsBytes(sigBlockTemplateUrl));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, byte[] signatureBlockTemplate) {
|
|
||||||
keySet = new KeySet(name, publicKey, privateKey, signatureBlockTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeys(String name, X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, byte[] signatureBlockTemplate) {
|
|
||||||
keySet = new KeySet(name, publicKey, privateKey, signatureAlgorithm, signatureBlockTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeySet getKeySet() {
|
|
||||||
return keySet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the operation to be canceled.
|
|
||||||
public void cancel() {
|
|
||||||
canceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the instance to sign again if previously canceled.
|
|
||||||
public void resetCanceled() {
|
|
||||||
canceled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCanceled() {
|
|
||||||
return canceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void loadProvider(String providerClassName)
|
|
||||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
|
||||||
Class providerClass = Class.forName(providerClassName);
|
|
||||||
Provider provider = (Provider) providerClass.newInstance();
|
|
||||||
Security.insertProviderAt(provider, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public X509Certificate readPublicKey(URL publicKeyUrl)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
InputStream input = publicKeyUrl.openStream();
|
|
||||||
try {
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an encrypted PKCS 8 format private key.
|
|
||||||
* <p>
|
|
||||||
* Based on ghstark's post on Aug 6, 2006 at
|
|
||||||
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
|
|
||||||
*
|
|
||||||
* @param encryptedPrivateKey The raw data of the private key
|
|
||||||
* @param keyPassword the key password
|
|
||||||
*/
|
|
||||||
private KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, String keyPassword)
|
|
||||||
throws GeneralSecurityException {
|
|
||||||
EncryptedPrivateKeyInfo epkInfo;
|
|
||||||
try {
|
|
||||||
epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
// Probably not an encrypted key.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
char[] keyPasswd = keyPassword.toCharArray();
|
|
||||||
|
|
||||||
SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
|
|
||||||
Key key = skFactory.generateSecret(new PBEKeySpec(keyPasswd));
|
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
|
|
||||||
|
|
||||||
try {
|
|
||||||
return epkInfo.getKeySpec(cipher);
|
|
||||||
} catch (InvalidKeySpecException ex) {
|
|
||||||
getLogger().error("signapk: Password for private key may be bad.");
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the content at the specified URL and return it as a byte array.
|
|
||||||
*/
|
|
||||||
public byte[] readContentAsBytes(URL contentUrl) throws IOException {
|
|
||||||
return readContentAsBytes(contentUrl.openStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the content from the given stream and return it as a byte array.
|
|
||||||
*/
|
|
||||||
public byte[] readContentAsBytes(InputStream input) throws IOException {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
byte[] buffer = new byte[2048];
|
|
||||||
|
|
||||||
int numRead = input.read(buffer);
|
|
||||||
while (numRead != -1) {
|
|
||||||
baos.write(buffer, 0, numRead);
|
|
||||||
numRead = input.read(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes = baos.toByteArray();
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a PKCS 8 format private key.
|
|
||||||
*/
|
|
||||||
public PrivateKey readPrivateKey(URL privateKeyUrl, String keyPassword)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
DataInputStream input = new DataInputStream(privateKeyUrl.openStream());
|
|
||||||
try {
|
|
||||||
byte[] bytes = readContentAsBytes(input);
|
|
||||||
|
|
||||||
KeySpec spec = decryptPrivateKey(bytes, keyPassword);
|
|
||||||
if (spec == null) {
|
|
||||||
spec = new PKCS8EncodedKeySpec(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return KeyFactory.getInstance("RSA").generatePrivate(spec);
|
|
||||||
} catch (InvalidKeySpecException ex) {
|
|
||||||
return KeyFactory.getInstance("DSA").generatePrivate(spec);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the SHA1 of every file to the manifest, creating it if necessary.
|
|
||||||
*/
|
|
||||||
private Manifest addDigestsToManifest(Map<String, ZioEntry> entries)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
Manifest input = null;
|
|
||||||
ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
|
|
||||||
if (manifestEntry != null) {
|
|
||||||
input = new Manifest();
|
|
||||||
input.read(manifestEntry.getInputStream());
|
|
||||||
}
|
|
||||||
Manifest output = new Manifest();
|
|
||||||
Attributes main = output.getMainAttributes();
|
|
||||||
if (input != null) {
|
|
||||||
main.putAll(input.getMainAttributes());
|
|
||||||
} else {
|
|
||||||
main.putValue("Manifest-Version", "1.0");
|
|
||||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// BASE64Encoder base64 = new BASE64Encoder();
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
byte[] buffer = new byte[512];
|
|
||||||
int num;
|
|
||||||
|
|
||||||
// We sort the input entries by name, and add them to the
|
|
||||||
// output manifest in sorted order. We expect that the output
|
|
||||||
// map will be deterministic.
|
|
||||||
|
|
||||||
TreeMap<String, ZioEntry> byName = new TreeMap<String, ZioEntry>();
|
|
||||||
byName.putAll(entries);
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
if (debug) getLogger().debug("Manifest entries:");
|
|
||||||
for (ZioEntry entry : byName.values()) {
|
|
||||||
if (canceled) break;
|
|
||||||
String name = entry.getName();
|
|
||||||
if (debug) getLogger().debug(name);
|
|
||||||
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
|
|
||||||
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
|
|
||||||
(stripPattern == null ||
|
|
||||||
!stripPattern.matcher(name).matches())) {
|
|
||||||
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_MANIFEST));
|
|
||||||
InputStream data = entry.getInputStream();
|
|
||||||
while ((num = data.read(buffer)) > 0) {
|
|
||||||
md.update(buffer, 0, num);
|
|
||||||
}
|
|
||||||
|
|
||||||
Attributes attr = null;
|
|
||||||
if (input != null) {
|
|
||||||
java.util.jar.Attributes inAttr = input.getAttributes(name);
|
|
||||||
if (inAttr != null) attr = new Attributes(inAttr);
|
|
||||||
}
|
|
||||||
if (attr == null) attr = new Attributes();
|
|
||||||
attr.putValue("SHA1-Digest", Base64.encode(md.digest()));
|
|
||||||
output.getEntries().put(name, attr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the signature file to the given output stream.
|
|
||||||
*/
|
|
||||||
private void generateSignatureFile(Manifest manifest, OutputStream out)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
out.write(("Signature-Version: 1.0\r\n").getBytes());
|
|
||||||
out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
|
|
||||||
|
|
||||||
|
|
||||||
// BASE64Encoder base64 = new BASE64Encoder();
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
PrintStream print = new PrintStream(
|
|
||||||
new DigestOutputStream(new ByteArrayOutputStream(), md),
|
|
||||||
true, "UTF-8");
|
|
||||||
|
|
||||||
// Digest of the entire manifest
|
|
||||||
manifest.write(print);
|
|
||||||
print.flush();
|
|
||||||
|
|
||||||
out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
|
||||||
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
|
||||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
|
||||||
if (canceled) break;
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_FILE));
|
|
||||||
// Digest of the manifest stanza for this entry.
|
|
||||||
String nameEntry = "Name: " + entry.getKey() + "\r\n";
|
|
||||||
print.print(nameEntry);
|
|
||||||
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
|
|
||||||
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
|
|
||||||
}
|
|
||||||
print.print("\r\n");
|
|
||||||
print.flush();
|
|
||||||
|
|
||||||
out.write(nameEntry.getBytes());
|
|
||||||
out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a .RSA file with a digital signature.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void writeSignatureBlock(KeySet keySet, byte[] signatureFileBytes, OutputStream out)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
if (keySet.getSigBlockTemplate() != null) {
|
|
||||||
|
|
||||||
// Can't use default Signature on Android. Although it generates a signature that can be verified by jarsigner,
|
|
||||||
// the recovery program appears to require a specific algorithm/mode/padding. So we use the custom ZipSignature instead.
|
|
||||||
// Signature signature = Signature.getInstance("SHA1withRSA");
|
|
||||||
ZipSignature signature = new ZipSignature();
|
|
||||||
signature.initSign(keySet.getPrivateKey());
|
|
||||||
signature.update(signatureFileBytes);
|
|
||||||
byte[] signatureBytes = signature.sign();
|
|
||||||
|
|
||||||
out.write(keySet.getSigBlockTemplate());
|
|
||||||
out.write(signatureBytes);
|
|
||||||
|
|
||||||
if (getLogger().isDebugEnabled()) {
|
|
||||||
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
md.update(signatureFileBytes);
|
|
||||||
byte[] sfDigest = md.digest();
|
|
||||||
getLogger().debug("Sig File SHA1: \n" + HexDumpEncoder.encode(sfDigest));
|
|
||||||
|
|
||||||
getLogger().debug("Signature: \n" + HexDumpEncoder.encode(signatureBytes));
|
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, keySet.getPublicKey());
|
|
||||||
|
|
||||||
byte[] tmpData = cipher.doFinal(signatureBytes);
|
|
||||||
getLogger().debug("Signature Decrypted: \n" + HexDumpEncoder.encode(tmpData));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
byte[] sigBlock = null;
|
|
||||||
// Use reflection to call the optional generator.
|
|
||||||
Class generatorClass = Class.forName("kellinwood.security.zipsigner.optional.SignatureBlockGenerator");
|
|
||||||
Method generatorMethod = generatorClass.getMethod("generate", KeySet.class, (new byte[1]).getClass());
|
|
||||||
sigBlock = (byte[]) generatorMethod.invoke(null, keySet, signatureFileBytes);
|
|
||||||
out.write(sigBlock);
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new RuntimeException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy all the files in a manifest from input to output. We set
|
|
||||||
* the modification times in the output to a fixed time, so as to
|
|
||||||
* reduce variation in the output file and make incremental OTAs
|
|
||||||
* more efficient.
|
|
||||||
*/
|
|
||||||
private void copyFiles(Manifest manifest, Map<String, ZioEntry> input, ZipOutput output, long timestamp)
|
|
||||||
throws IOException {
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
|
||||||
List<String> names = new ArrayList<String>(entries.keySet());
|
|
||||||
Collections.sort(names);
|
|
||||||
int i = 1;
|
|
||||||
for (String name : names) {
|
|
||||||
if (canceled) break;
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, names.size()));
|
|
||||||
i += 1;
|
|
||||||
ZioEntry inEntry = input.get(name);
|
|
||||||
inEntry.setTime(timestamp);
|
|
||||||
output.write(inEntry);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy all the files from input to output.
|
|
||||||
*/
|
|
||||||
private void copyFiles(Map<String, ZioEntry> input, ZipOutput output)
|
|
||||||
throws IOException {
|
|
||||||
int i = 1;
|
|
||||||
for (ZioEntry inEntry : input.values()) {
|
|
||||||
if (canceled) break;
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.COPYING_ZIP_ENTRY, i, input.size()));
|
|
||||||
i += 1;
|
|
||||||
output.write(inEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated - use the version that takes the passwords as char[]
|
|
||||||
*/
|
|
||||||
public void signZip(URL keystoreURL,
|
|
||||||
String keystoreType,
|
|
||||||
String keystorePw,
|
|
||||||
String certAlias,
|
|
||||||
String certPw,
|
|
||||||
String inputZipFilename,
|
|
||||||
String outputZipFilename)
|
|
||||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
|
|
||||||
IOException, GeneralSecurityException {
|
|
||||||
signZip(keystoreURL, keystoreType, keystorePw.toCharArray(), certAlias, certPw.toCharArray(), "SHA1withRSA", inputZipFilename, outputZipFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void signZip(URL keystoreURL,
|
|
||||||
String keystoreType,
|
|
||||||
char[] keystorePw,
|
|
||||||
String certAlias,
|
|
||||||
char[] certPw,
|
|
||||||
String signatureAlgorithm,
|
|
||||||
String inputZipFilename,
|
|
||||||
String outputZipFilename)
|
|
||||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
|
|
||||||
IOException, GeneralSecurityException {
|
|
||||||
InputStream keystoreStream = null;
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
KeyStore keystore = null;
|
|
||||||
if (keystoreType == null) keystoreType = KeyStore.getDefaultType();
|
|
||||||
keystore = KeyStore.getInstance(keystoreType);
|
|
||||||
|
|
||||||
keystoreStream = keystoreURL.openStream();
|
|
||||||
keystore.load(keystoreStream, keystorePw);
|
|
||||||
Certificate cert = keystore.getCertificate(certAlias);
|
|
||||||
X509Certificate publicKey = (X509Certificate) cert;
|
|
||||||
Key key = keystore.getKey(certAlias, certPw);
|
|
||||||
PrivateKey privateKey = (PrivateKey) key;
|
|
||||||
|
|
||||||
setKeys("custom", publicKey, privateKey, signatureAlgorithm, null);
|
|
||||||
|
|
||||||
signZip(inputZipFilename, outputZipFilename);
|
|
||||||
} finally {
|
|
||||||
if (keystoreStream != null) keystoreStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign the input with the default test key and certificate.
|
|
||||||
* Save result to output file.
|
|
||||||
*/
|
|
||||||
public void signZip(Map<String, ZioEntry> zioEntries, String outputZipFilename)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
progressHelper.initProgress();
|
|
||||||
signZip(zioEntries, new FileOutputStream(outputZipFilename), outputZipFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign the file using the given public key cert, private key,
|
|
||||||
* and signature block template. The signature block template
|
|
||||||
* parameter may be null, but if so
|
|
||||||
* android-sun-jarsign-support.jar must be in the classpath.
|
|
||||||
*/
|
|
||||||
public void signZip(String inputZipFilename, String outputZipFilename)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
File inFile = new File(inputZipFilename).getCanonicalFile();
|
|
||||||
File outFile = new File(outputZipFilename).getCanonicalFile();
|
|
||||||
|
|
||||||
if (inFile.equals(outFile)) {
|
|
||||||
throw new IllegalArgumentException(resourceAdapter.getString(ResourceAdapter.Item.INPUT_SAME_AS_OUTPUT_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
progressHelper.initProgress();
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, resourceAdapter.getString(ResourceAdapter.Item.PARSING_CENTRAL_DIRECTORY));
|
|
||||||
|
|
||||||
ZipInput input = null;
|
|
||||||
OutputStream outStream = null;
|
|
||||||
try {
|
|
||||||
input = ZipInput.read(inputZipFilename);
|
|
||||||
outStream = new FileOutputStream(outputZipFilename);
|
|
||||||
signZip(input.getEntries(), outStream, outputZipFilename);
|
|
||||||
} finally {
|
|
||||||
if (input != null) input.close();
|
|
||||||
if (outStream != null) outStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign the
|
|
||||||
* and signature block template. The signature block template
|
|
||||||
* parameter may be null, but if so
|
|
||||||
* android-sun-jarsign-support.jar must be in the classpath.
|
|
||||||
*/
|
|
||||||
public void signZip(Map<String, ZioEntry> zioEntries, OutputStream outputStream, String outputZipFilename)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
progressHelper.initProgress();
|
|
||||||
if (keySet == null) {
|
|
||||||
if (!keymode.startsWith(MODE_AUTO))
|
|
||||||
throw new IllegalStateException("No keys configured for signing the file!");
|
|
||||||
|
|
||||||
// Auto-determine which keys to use
|
|
||||||
String keyName = this.autoDetectKey(keymode, zioEntries);
|
|
||||||
if (keyName == null)
|
|
||||||
throw new AutoKeyException(resourceAdapter.getString(ResourceAdapter.Item.AUTO_KEY_SELECTION_ERROR, new File(outputZipFilename).getName()));
|
|
||||||
|
|
||||||
autoKeyObservable.notifyObservers(keyName);
|
|
||||||
|
|
||||||
loadKeys(keyName);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ZipOutput zipOutput = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
|
||||||
zipOutput = new ZipOutput(outputStream);
|
|
||||||
|
|
||||||
if (KEY_NONE.equals(keySet.getName())) {
|
|
||||||
progressHelper.setProgressTotalItems(zioEntries.size());
|
|
||||||
progressHelper.setProgressCurrentItem(0);
|
|
||||||
copyFiles(zioEntries, zipOutput);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total steps to complete for accurate progress percentages.
|
|
||||||
int progressTotalItems = 0;
|
|
||||||
for (ZioEntry entry : zioEntries.values()) {
|
|
||||||
String name = entry.getName();
|
|
||||||
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
|
|
||||||
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
|
|
||||||
(stripPattern == null ||
|
|
||||||
!stripPattern.matcher(name).matches())) {
|
|
||||||
progressTotalItems += 3; // digest for manifest, digest in sig file, copy data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progressTotalItems += 1; // CERT.RSA generation
|
|
||||||
progressHelper.setProgressTotalItems(progressTotalItems);
|
|
||||||
progressHelper.setProgressCurrentItem(0);
|
|
||||||
|
|
||||||
// Assume the certificate is valid for at least an hour.
|
|
||||||
long timestamp = keySet.getPublicKey().getNotBefore().getTime() + 3600L * 1000;
|
|
||||||
|
|
||||||
// MANIFEST.MF
|
|
||||||
// progress(ProgressEvent.PRORITY_NORMAL, JarFile.MANIFEST_NAME);
|
|
||||||
Manifest manifest = addDigestsToManifest(zioEntries);
|
|
||||||
if (canceled) return;
|
|
||||||
ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME);
|
|
||||||
ze.setTime(timestamp);
|
|
||||||
manifest.write(ze.getOutputStream());
|
|
||||||
zipOutput.write(ze);
|
|
||||||
|
|
||||||
|
|
||||||
// CERT.SF
|
|
||||||
ze = new ZioEntry(CERT_SF_NAME);
|
|
||||||
ze.setTime(timestamp);
|
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
generateSignatureFile(manifest, out);
|
|
||||||
if (canceled) return;
|
|
||||||
byte[] sfBytes = out.toByteArray();
|
|
||||||
if (debug) {
|
|
||||||
getLogger().debug("Signature File: \n" + new String(sfBytes) + "\n" +
|
|
||||||
HexDumpEncoder.encode(sfBytes));
|
|
||||||
}
|
|
||||||
ze.getOutputStream().write(sfBytes);
|
|
||||||
zipOutput.write(ze);
|
|
||||||
|
|
||||||
// CERT.RSA
|
|
||||||
progressHelper.progress(ProgressEvent.PRORITY_NORMAL, resourceAdapter.getString(ResourceAdapter.Item.GENERATING_SIGNATURE_BLOCK));
|
|
||||||
ze = new ZioEntry(CERT_RSA_NAME);
|
|
||||||
ze.setTime(timestamp);
|
|
||||||
writeSignatureBlock(keySet, sfBytes, ze.getOutputStream());
|
|
||||||
zipOutput.write(ze);
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
// Everything else
|
|
||||||
copyFiles(manifest, zioEntries, zipOutput, timestamp);
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (zipOutput != null) zipOutput.close();
|
|
||||||
if (canceled) {
|
|
||||||
try {
|
|
||||||
if (outputZipFilename != null) new File(outputZipFilename).delete();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
getLogger().warning(t.getClass().getName() + ":" + t.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addProgressListener(ProgressListener l) {
|
|
||||||
progressHelper.addProgressListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void removeProgressListener(ProgressListener l) {
|
|
||||||
progressHelper.removeProgressListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class AutoKeyObservable extends Observable {
|
|
||||||
@Override
|
|
||||||
public void notifyObservers(Object arg) {
|
|
||||||
super.setChanged();
|
|
||||||
super.notifyObservers(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import kellinwood.security.zipsigner.KeySet;
|
|
||||||
import org.bouncycastle.jce.X509Principal;
|
|
||||||
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.KeyPairGenerator;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All methods create self-signed certificates.
|
|
||||||
*/
|
|
||||||
public class CertCreator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new keystore and self-signed key. The key will have the same password as the key, and will be
|
|
||||||
* RSA 2048, with the cert signed using SHA1withRSA. The certificate will have a validity of
|
|
||||||
* 30 years).
|
|
||||||
*
|
|
||||||
* @param storePath - pathname of the new keystore file
|
|
||||||
* @param password - keystore and key password
|
|
||||||
* @param keyName - the new key will have this as its alias within the keystore
|
|
||||||
* @param distinguishedNameValues - contains Country, State, Locality,...,Common Name, etc.
|
|
||||||
*/
|
|
||||||
public static void createKeystoreAndKey(String storePath, char[] password,
|
|
||||||
String keyName, DistinguishedNameValues distinguishedNameValues) {
|
|
||||||
createKeystoreAndKey(storePath, password, "RSA", 2048, keyName, password, "SHA1withRSA", 30,
|
|
||||||
distinguishedNameValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static KeySet createKeystoreAndKey(String storePath, char[] storePass,
|
|
||||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
|
||||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
|
||||||
distinguishedNameValues);
|
|
||||||
|
|
||||||
|
|
||||||
KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePath, storePass);
|
|
||||||
|
|
||||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
|
||||||
keyPass,
|
|
||||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
|
||||||
|
|
||||||
File sfile = new File(storePath);
|
|
||||||
if (sfile.exists()) {
|
|
||||||
throw new IOException("File already exists: " + storePath);
|
|
||||||
}
|
|
||||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
|
||||||
|
|
||||||
return keySet;
|
|
||||||
} catch (RuntimeException x) {
|
|
||||||
throw x;
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new RuntimeException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new key and store it in an existing keystore.
|
|
||||||
*/
|
|
||||||
public static KeySet createKey(String storePath, char[] storePass,
|
|
||||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
|
||||||
String certSignatureAlgorithm, int certValidityYears,
|
|
||||||
DistinguishedNameValues distinguishedNameValues) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
|
||||||
distinguishedNameValues);
|
|
||||||
|
|
||||||
KeyStore privateKS = KeyStoreFileManager.loadKeyStore(storePath, storePass);
|
|
||||||
|
|
||||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
|
||||||
keyPass,
|
|
||||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
|
||||||
|
|
||||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
|
||||||
|
|
||||||
return keySet;
|
|
||||||
|
|
||||||
} catch (RuntimeException x) {
|
|
||||||
throw x;
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new RuntimeException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeySet createKey(String keyAlgorithm, int keySize, String keyName,
|
|
||||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
|
||||||
try {
|
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
|
|
||||||
keyPairGenerator.initialize(keySize);
|
|
||||||
KeyPair KPair = keyPairGenerator.generateKeyPair();
|
|
||||||
|
|
||||||
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
|
|
||||||
|
|
||||||
X509Principal principal = distinguishedNameValues.getPrincipal();
|
|
||||||
|
|
||||||
// generate a postitive serial number
|
|
||||||
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
|
||||||
while (serialNumber.compareTo(BigInteger.ZERO) < 0) {
|
|
||||||
serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
|
||||||
}
|
|
||||||
v3CertGen.setSerialNumber(serialNumber);
|
|
||||||
v3CertGen.setIssuerDN(principal);
|
|
||||||
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
|
|
||||||
v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * (long) certValidityYears)));
|
|
||||||
v3CertGen.setSubjectDN(principal);
|
|
||||||
|
|
||||||
v3CertGen.setPublicKey(KPair.getPublic());
|
|
||||||
v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
|
|
||||||
|
|
||||||
X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(), "BC");
|
|
||||||
|
|
||||||
KeySet keySet = new KeySet();
|
|
||||||
keySet.setName(keyName);
|
|
||||||
keySet.setPrivateKey(KPair.getPrivate());
|
|
||||||
keySet.setPublicKey(PKCertificate);
|
|
||||||
return keySet;
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new RuntimeException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import kellinwood.security.zipsigner.ZipSigner;
|
|
||||||
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public class CustomKeySigner {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* KeyStore-type agnostic. This method will sign the zip file, automatically handling JKS or BKS keystores.
|
|
||||||
*/
|
|
||||||
public static void signZip(ZipSigner zipSigner,
|
|
||||||
String keystorePath,
|
|
||||||
char[] keystorePw,
|
|
||||||
String certAlias,
|
|
||||||
char[] certPw,
|
|
||||||
String signatureAlgorithm,
|
|
||||||
String inputZipFilename,
|
|
||||||
String outputZipFilename)
|
|
||||||
throws Exception {
|
|
||||||
zipSigner.issueLoadingCertAndKeysProgressEvent();
|
|
||||||
KeyStore keystore = KeyStoreFileManager.loadKeyStore(keystorePath, keystorePw);
|
|
||||||
Certificate cert = keystore.getCertificate(certAlias);
|
|
||||||
X509Certificate publicKey = (X509Certificate) cert;
|
|
||||||
Key key = keystore.getKey(certAlias, certPw);
|
|
||||||
PrivateKey privateKey = (PrivateKey) key;
|
|
||||||
|
|
||||||
zipSigner.setKeys("custom", publicKey, privateKey, signatureAlgorithm, null);
|
|
||||||
zipSigner.signZip(inputZipFilename, outputZipFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
|
||||||
import org.bouncycastle.jce.X509Principal;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for dealing with the distinguished name RDNs.
|
|
||||||
*/
|
|
||||||
public class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier, String> {
|
|
||||||
|
|
||||||
public DistinguishedNameValues() {
|
|
||||||
put(BCStyle.C, null);
|
|
||||||
put(BCStyle.ST, null);
|
|
||||||
put(BCStyle.L, null);
|
|
||||||
put(BCStyle.STREET, null);
|
|
||||||
put(BCStyle.O, null);
|
|
||||||
put(BCStyle.OU, null);
|
|
||||||
put(BCStyle.CN, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String put(ASN1ObjectIdentifier oid, String value) {
|
|
||||||
if (value != null && value.equals("")) value = null;
|
|
||||||
if (containsKey(oid)) super.put(oid, value); // preserve original ordering
|
|
||||||
else {
|
|
||||||
super.put(oid, value);
|
|
||||||
// String cn = remove(BCStyle.CN); // CN will always be last.
|
|
||||||
// put(BCStyle.CN,cn);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCountry(String country) {
|
|
||||||
put(BCStyle.C, country);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setState(String state) {
|
|
||||||
put(BCStyle.ST, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocality(String locality) {
|
|
||||||
put(BCStyle.L, locality);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreet(String street) {
|
|
||||||
put(BCStyle.STREET, street);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOrganization(String organization) {
|
|
||||||
put(BCStyle.O, organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOrganizationalUnit(String organizationalUnit) {
|
|
||||||
put(BCStyle.OU, organizationalUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCommonName(String commonName) {
|
|
||||||
put(BCStyle.CN, commonName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
int result = 0;
|
|
||||||
for (String value : values()) {
|
|
||||||
if (value != null) result += 1;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Principal getPrincipal() {
|
|
||||||
Vector<ASN1ObjectIdentifier> oids = new Vector<ASN1ObjectIdentifier>();
|
|
||||||
Vector<String> values = new Vector<String>();
|
|
||||||
|
|
||||||
for (Map.Entry<ASN1ObjectIdentifier, String> entry : entrySet()) {
|
|
||||||
if (entry.getValue() != null && !entry.getValue().equals("")) {
|
|
||||||
oids.add(entry.getKey());
|
|
||||||
values.add(entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new X509Principal(oids, values);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
import kellinwood.security.zipsigner.Base64;
|
|
||||||
import org.bouncycastle.util.encoders.HexTranslator;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User: ken
|
|
||||||
* Date: 1/17/13
|
|
||||||
*/
|
|
||||||
public class Fingerprint {
|
|
||||||
|
|
||||||
static LoggerInterface logger = LoggerManager.getLogger(Fingerprint.class.getName());
|
|
||||||
|
|
||||||
static byte[] calcDigest(String algorithm, byte[] encodedCert) {
|
|
||||||
byte[] result = null;
|
|
||||||
try {
|
|
||||||
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
|
|
||||||
messageDigest.update(encodedCert);
|
|
||||||
result = messageDigest.digest();
|
|
||||||
} catch (Exception x) {
|
|
||||||
logger.error(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String hexFingerprint(String algorithm, byte[] encodedCert) {
|
|
||||||
try {
|
|
||||||
byte[] digest = calcDigest(algorithm, encodedCert);
|
|
||||||
if (digest == null) return null;
|
|
||||||
HexTranslator hexTranslator = new HexTranslator();
|
|
||||||
byte[] hex = new byte[digest.length * 2];
|
|
||||||
hexTranslator.encode(digest, 0, digest.length, hex, 0);
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (int i = 0; i < hex.length; i += 2) {
|
|
||||||
builder.append((char) hex[i]);
|
|
||||||
builder.append((char) hex[i + 1]);
|
|
||||||
if (i != (hex.length - 2)) builder.append(':');
|
|
||||||
}
|
|
||||||
return builder.toString().toUpperCase(Locale.ENGLISH);
|
|
||||||
} catch (Exception x) {
|
|
||||||
logger.error(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public static void main(String[] args) {
|
|
||||||
// byte[] data = "The Silence of the Lambs is a really good movie.".getBytes();
|
|
||||||
// System.out.println(hexFingerprint("MD5", data));
|
|
||||||
// System.out.println(hexFingerprint("SHA1", data));
|
|
||||||
// System.out.println(base64Fingerprint("SHA1", data));
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
public static String base64Fingerprint(String algorithm, byte[] encodedCert) {
|
|
||||||
String result = null;
|
|
||||||
try {
|
|
||||||
byte[] digest = calcDigest(algorithm, encodedCert);
|
|
||||||
if (digest == null) return result;
|
|
||||||
return Base64.encode(digest);
|
|
||||||
} catch (Exception x) {
|
|
||||||
logger.error(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,500 +0,0 @@
|
|||||||
/* JKS.java -- implementation of the "JKS" key store.
|
|
||||||
Copyright (C) 2003 Casey Marshall <rsdio@metastatic.org>
|
|
||||||
|
|
||||||
Permission to use, copy, modify, distribute, and sell this software and
|
|
||||||
its documentation for any purpose is hereby granted without fee,
|
|
||||||
provided that the above copyright notice appear in all copies and that
|
|
||||||
both that copyright notice and this permission notice appear in
|
|
||||||
supporting documentation. No representations are made about the
|
|
||||||
suitability of this software for any purpose. It is provided "as is"
|
|
||||||
without express or implied warranty.
|
|
||||||
|
|
||||||
This program was derived by reverse-engineering Sun's own
|
|
||||||
implementation, using only the public API that is available in the 1.4.1
|
|
||||||
JDK. Hence nothing in this program is, or is derived from, anything
|
|
||||||
copyrighted by Sun Microsystems. While the "Binary Evaluation License
|
|
||||||
Agreement" that the JDK is licensed under contains blanket statements
|
|
||||||
that forbid reverse-engineering (among other things), it is my position
|
|
||||||
that US copyright law does not and cannot forbid reverse-engineering of
|
|
||||||
software to produce a compatible implementation. There are, in fact,
|
|
||||||
numerous clauses in copyright law that specifically allow
|
|
||||||
reverse-engineering, and therefore I believe it is outside of Sun's
|
|
||||||
power to enforce restrictions on reverse-engineering of their software,
|
|
||||||
and it is irresponsible for them to claim they can. */
|
|
||||||
|
|
||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.security.DigestInputStream;
|
|
||||||
import java.security.DigestOutputStream;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.KeyStoreSpi;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.UnrecoverableKeyException;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an implementation of Sun's proprietary key store
|
|
||||||
* algorithm, called "JKS" for "Java Key Store". This implementation was
|
|
||||||
* created entirely through reverse-engineering.
|
|
||||||
* <p>
|
|
||||||
* <p>The format of JKS files is, from the start of the file:
|
|
||||||
* <p>
|
|
||||||
* <ol>
|
|
||||||
* <li>Magic bytes. This is a four-byte integer, in big-endian byte
|
|
||||||
* order, equal to <code>0xFEEDFEED</code>.</li>
|
|
||||||
* <li>The version number (probably), as a four-byte integer (all
|
|
||||||
* multibyte integral types are in big-endian byte order). The current
|
|
||||||
* version number (in modern distributions of the JDK) is 2.</li>
|
|
||||||
* <li>The number of entrires in this keystore, as a four-byte
|
|
||||||
* integer. Call this value <i>n</i></li>
|
|
||||||
* <li>Then, <i>n</i> times:
|
|
||||||
* <ol>
|
|
||||||
* <li>The entry type, a four-byte int. The value 1 denotes a private
|
|
||||||
* key entry, and 2 denotes a trusted certificate.</li>
|
|
||||||
* <li>The entry's alias, formatted as strings such as those written
|
|
||||||
* by <a
|
|
||||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)">DataOutput.writeUTF(String)</a>.</li>
|
|
||||||
* <li>An eight-byte integer, representing the entry's creation date,
|
|
||||||
* in milliseconds since the epoch.
|
|
||||||
* <p>
|
|
||||||
* <p>Then, if the entry is a private key entry:
|
|
||||||
* <ol>
|
|
||||||
* <li>The size of the encoded key as a four-byte int, then that
|
|
||||||
* number of bytes. The encoded key is the DER encoded bytes of the
|
|
||||||
* <a
|
|
||||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a> structure (the
|
|
||||||
* encryption algorithm is discussed later).</li>
|
|
||||||
* <li>A four-byte integer, followed by that many encoded
|
|
||||||
* certificates, encoded as described in the trusted certificates
|
|
||||||
* section.</li>
|
|
||||||
* </ol>
|
|
||||||
* <p>
|
|
||||||
* <p>Otherwise, the entry is a trusted certificate, which is encoded
|
|
||||||
* as the name of the encoding algorithm (e.g. X.509), encoded the same
|
|
||||||
* way as alias names. Then, a four-byte integer representing the size
|
|
||||||
* of the encoded certificate, then that many bytes representing the
|
|
||||||
* encoded certificate (e.g. the DER bytes in the case of X.509).
|
|
||||||
* </li>
|
|
||||||
* </ol>
|
|
||||||
* </li>
|
|
||||||
* <li>Then, the signature.</li>
|
|
||||||
* </ol>
|
|
||||||
* </ol>
|
|
||||||
* </li>
|
|
||||||
* </ol>
|
|
||||||
* <p>
|
|
||||||
* <p>(See <a href="genkey.java">this file</a> for some idea of how I
|
|
||||||
* was able to figure out these algorithms)</p>
|
|
||||||
* <p>
|
|
||||||
* <p>Decrypting the key works as follows:
|
|
||||||
* <p>
|
|
||||||
* <ol>
|
|
||||||
* <li>The key length is the length of the ciphertext minus 40. The
|
|
||||||
* encrypted key, <code>ekey</code>, is the middle bytes of the
|
|
||||||
* ciphertext.</li>
|
|
||||||
* <li>Take the first 20 bytes of the encrypted key as a seed value,
|
|
||||||
* <code>K[0]</code>.</li>
|
|
||||||
* <li>Compute <code>K[1] ... K[n]</code>, where
|
|
||||||
* <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
|
|
||||||
* <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
|
|
||||||
* <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
|
|
||||||
* <li>The last 20 bytes are the checksum, computed as <code>H =
|
|
||||||
* SHA-1(UTF-16BE(password) + key)</code>. If this value does not match
|
|
||||||
* the last 20 bytes of the ciphertext, output <code>FAIL</code>. Otherwise,
|
|
||||||
* output <code>key</code>.</li>
|
|
||||||
* </ol>
|
|
||||||
* <p>
|
|
||||||
* <p>The signature is defined as <code>SHA-1(UTF-16BE(password) +
|
|
||||||
* US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun
|
|
||||||
* engineers are just that clever).
|
|
||||||
* <p>
|
|
||||||
* <p>(Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the
|
|
||||||
* big-endian byte representation of a UTF-16 string, and US_ASCII the
|
|
||||||
* ASCII byte representation of the string.)
|
|
||||||
* <p>
|
|
||||||
* <p>The source code of this class should be available in the file <a
|
|
||||||
* href="http://metastatic.org/source/JKS.java">JKS.java</a>.
|
|
||||||
*
|
|
||||||
* @author Casey Marshall (rsdio@metastatic.org)
|
|
||||||
* <p>
|
|
||||||
* Changes by Ken Ellinwood:
|
|
||||||
* ** Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is null.
|
|
||||||
* ** engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.
|
|
||||||
* ** Lowercase the alias names, otherwise keytool chokes on the file created by this code.
|
|
||||||
* ** Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password value.
|
|
||||||
*/
|
|
||||||
public class JKS extends KeyStoreSpi {
|
|
||||||
|
|
||||||
// Constants and fields.
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ah, Sun. So goddamned clever with those magic bytes.
|
|
||||||
*/
|
|
||||||
private static final int MAGIC = 0xFEEDFEED;
|
|
||||||
|
|
||||||
private static final int PRIVATE_KEY = 1;
|
|
||||||
private static final int TRUSTED_CERT = 2;
|
|
||||||
|
|
||||||
private final Vector aliases;
|
|
||||||
private final HashMap trustedCerts;
|
|
||||||
private final HashMap privateKeys;
|
|
||||||
private final HashMap certChains;
|
|
||||||
private final HashMap dates;
|
|
||||||
|
|
||||||
// Constructor.
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public JKS() {
|
|
||||||
super();
|
|
||||||
aliases = new Vector();
|
|
||||||
trustedCerts = new HashMap();
|
|
||||||
privateKeys = new HashMap();
|
|
||||||
certChains = new HashMap();
|
|
||||||
dates = new HashMap();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instance methods.
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public Key engineGetKey(String alias, char[] password)
|
|
||||||
throws NoSuchAlgorithmException, UnrecoverableKeyException {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
|
|
||||||
if (!privateKeys.containsKey(alias))
|
|
||||||
return null;
|
|
||||||
byte[] key = decryptKey((byte[]) privateKeys.get(alias),
|
|
||||||
charsToBytes(password));
|
|
||||||
Certificate[] chain = engineGetCertificateChain(alias);
|
|
||||||
if (chain.length > 0) {
|
|
||||||
try {
|
|
||||||
// Private and public keys MUST have the same algorithm.
|
|
||||||
KeyFactory fact = KeyFactory.getInstance(
|
|
||||||
chain[0].getPublicKey().getAlgorithm());
|
|
||||||
return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
|
|
||||||
} catch (InvalidKeySpecException x) {
|
|
||||||
throw new UnrecoverableKeyException(x.getMessage());
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
return new SecretKeySpec(key, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Certificate[] engineGetCertificateChain(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
return (Certificate[]) certChains.get(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Certificate engineGetCertificate(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
if (engineIsKeyEntry(alias)) {
|
|
||||||
Certificate[] certChain = (Certificate[]) certChains.get(alias);
|
|
||||||
if (certChain != null && certChain.length > 0) return certChain[0];
|
|
||||||
}
|
|
||||||
return (Certificate) trustedCerts.get(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date engineGetCreationDate(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
return (Date) dates.get(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX implement writing methods.
|
|
||||||
|
|
||||||
public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
|
|
||||||
throws KeyStoreException {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
if (trustedCerts.containsKey(alias))
|
|
||||||
throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
|
|
||||||
privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
|
|
||||||
if (certChain != null)
|
|
||||||
certChains.put(alias, certChain);
|
|
||||||
else
|
|
||||||
certChains.put(alias, new Certificate[0]);
|
|
||||||
if (!aliases.contains(alias)) {
|
|
||||||
dates.put(alias, new Date());
|
|
||||||
aliases.add(alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain)
|
|
||||||
throws KeyStoreException {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
if (trustedCerts.containsKey(alias))
|
|
||||||
throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
|
|
||||||
try {
|
|
||||||
new EncryptedPrivateKeyInfo(encodedKey);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
|
|
||||||
}
|
|
||||||
privateKeys.put(alias, encodedKey);
|
|
||||||
if (certChain != null)
|
|
||||||
certChains.put(alias, certChain);
|
|
||||||
else
|
|
||||||
certChains.put(alias, new Certificate[0]);
|
|
||||||
if (!aliases.contains(alias)) {
|
|
||||||
dates.put(alias, new Date());
|
|
||||||
aliases.add(alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void engineSetCertificateEntry(String alias, Certificate cert)
|
|
||||||
throws KeyStoreException {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
if (privateKeys.containsKey(alias))
|
|
||||||
throw new KeyStoreException("\"" + alias + "\" is a private key entry");
|
|
||||||
if (cert == null)
|
|
||||||
throw new NullPointerException();
|
|
||||||
trustedCerts.put(alias, cert);
|
|
||||||
if (!aliases.contains(alias)) {
|
|
||||||
dates.put(alias, new Date());
|
|
||||||
aliases.add(alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void engineDeleteEntry(String alias) throws KeyStoreException {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
aliases.remove(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Enumeration engineAliases() {
|
|
||||||
return aliases.elements();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean engineContainsAlias(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
return aliases.contains(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int engineSize() {
|
|
||||||
return aliases.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean engineIsKeyEntry(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
return privateKeys.containsKey(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean engineIsCertificateEntry(String alias) {
|
|
||||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
|
||||||
return trustedCerts.containsKey(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String engineGetCertificateAlias(Certificate cert) {
|
|
||||||
for (Iterator keys = trustedCerts.keySet().iterator(); keys.hasNext(); ) {
|
|
||||||
String alias = (String) keys.next();
|
|
||||||
if (cert.equals(trustedCerts.get(alias)))
|
|
||||||
return alias;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void engineStore(OutputStream out, char[] passwd)
|
|
||||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
md.update(charsToBytes(passwd));
|
|
||||||
md.update("Mighty Aphrodite".getBytes("UTF-8"));
|
|
||||||
DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
|
|
||||||
dout.writeInt(MAGIC);
|
|
||||||
dout.writeInt(2);
|
|
||||||
dout.writeInt(aliases.size());
|
|
||||||
for (Enumeration e = aliases.elements(); e.hasMoreElements(); ) {
|
|
||||||
String alias = (String) e.nextElement();
|
|
||||||
if (trustedCerts.containsKey(alias)) {
|
|
||||||
dout.writeInt(TRUSTED_CERT);
|
|
||||||
dout.writeUTF(alias);
|
|
||||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
|
||||||
writeCert(dout, (Certificate) trustedCerts.get(alias));
|
|
||||||
} else {
|
|
||||||
dout.writeInt(PRIVATE_KEY);
|
|
||||||
dout.writeUTF(alias);
|
|
||||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
|
||||||
byte[] key = (byte[]) privateKeys.get(alias);
|
|
||||||
dout.writeInt(key.length);
|
|
||||||
dout.write(key);
|
|
||||||
Certificate[] chain = (Certificate[]) certChains.get(alias);
|
|
||||||
dout.writeInt(chain.length);
|
|
||||||
for (int i = 0; i < chain.length; i++)
|
|
||||||
writeCert(dout, chain[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
byte[] digest = md.digest();
|
|
||||||
dout.write(digest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void engineLoad(InputStream in, char[] passwd)
|
|
||||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA");
|
|
||||||
if (passwd != null) md.update(charsToBytes(passwd));
|
|
||||||
md.update("Mighty Aphrodite".getBytes("UTF-8")); // HAR HAR
|
|
||||||
aliases.clear();
|
|
||||||
trustedCerts.clear();
|
|
||||||
privateKeys.clear();
|
|
||||||
certChains.clear();
|
|
||||||
dates.clear();
|
|
||||||
if (in == null) return;
|
|
||||||
DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
|
|
||||||
if (din.readInt() != MAGIC)
|
|
||||||
throw new IOException("not a JavaKeyStore");
|
|
||||||
din.readInt(); // version no.
|
|
||||||
final int n = din.readInt();
|
|
||||||
aliases.ensureCapacity(n);
|
|
||||||
if (n < 0)
|
|
||||||
throw new LoadKeystoreException("Malformed key store");
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
int type = din.readInt();
|
|
||||||
String alias = din.readUTF();
|
|
||||||
aliases.add(alias);
|
|
||||||
dates.put(alias, new Date(din.readLong()));
|
|
||||||
switch (type) {
|
|
||||||
case PRIVATE_KEY:
|
|
||||||
int len = din.readInt();
|
|
||||||
byte[] encoded = new byte[len];
|
|
||||||
din.read(encoded);
|
|
||||||
privateKeys.put(alias, encoded);
|
|
||||||
int count = din.readInt();
|
|
||||||
Certificate[] chain = new Certificate[count];
|
|
||||||
for (int j = 0; j < count; j++)
|
|
||||||
chain[j] = readCert(din);
|
|
||||||
certChains.put(alias, chain);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TRUSTED_CERT:
|
|
||||||
trustedCerts.put(alias, readCert(din));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new LoadKeystoreException("Malformed key store");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passwd != null) {
|
|
||||||
byte[] computedHash = md.digest();
|
|
||||||
byte[] storedHash = new byte[20];
|
|
||||||
din.read(storedHash);
|
|
||||||
if (!MessageDigest.isEqual(storedHash, computedHash)) {
|
|
||||||
throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Own methods.
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static Certificate readCert(DataInputStream in)
|
|
||||||
throws IOException, CertificateException, NoSuchAlgorithmException {
|
|
||||||
String type = in.readUTF();
|
|
||||||
int len = in.readInt();
|
|
||||||
byte[] encoded = new byte[len];
|
|
||||||
in.read(encoded);
|
|
||||||
CertificateFactory factory = CertificateFactory.getInstance(type);
|
|
||||||
return factory.generateCertificate(new ByteArrayInputStream(encoded));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeCert(DataOutputStream dout, Certificate cert)
|
|
||||||
throws IOException, CertificateException {
|
|
||||||
dout.writeUTF(cert.getType());
|
|
||||||
byte[] b = cert.getEncoded();
|
|
||||||
dout.writeInt(b.length);
|
|
||||||
dout.write(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd)
|
|
||||||
throws UnrecoverableKeyException {
|
|
||||||
try {
|
|
||||||
EncryptedPrivateKeyInfo epki =
|
|
||||||
new EncryptedPrivateKeyInfo(encryptedPKI);
|
|
||||||
byte[] encr = epki.getEncryptedData();
|
|
||||||
byte[] keystream = new byte[20];
|
|
||||||
System.arraycopy(encr, 0, keystream, 0, 20);
|
|
||||||
byte[] check = new byte[20];
|
|
||||||
System.arraycopy(encr, encr.length - 20, check, 0, 20);
|
|
||||||
byte[] key = new byte[encr.length - 40];
|
|
||||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
|
||||||
int count = 0;
|
|
||||||
while (count < key.length) {
|
|
||||||
sha.reset();
|
|
||||||
sha.update(passwd);
|
|
||||||
sha.update(keystream);
|
|
||||||
sha.digest(keystream, 0, keystream.length);
|
|
||||||
for (int i = 0; i < keystream.length && count < key.length; i++) {
|
|
||||||
key[count] = (byte) (keystream[i] ^ encr[count + 20]);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sha.reset();
|
|
||||||
sha.update(passwd);
|
|
||||||
sha.update(key);
|
|
||||||
if (!MessageDigest.isEqual(check, sha.digest()))
|
|
||||||
throw new UnrecoverableKeyException("checksum mismatch");
|
|
||||||
return key;
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new UnrecoverableKeyException(x.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] encryptKey(Key key, byte[] passwd)
|
|
||||||
throws KeyStoreException {
|
|
||||||
try {
|
|
||||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
|
||||||
SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
|
|
||||||
byte[] k = key.getEncoded();
|
|
||||||
byte[] encrypted = new byte[k.length + 40];
|
|
||||||
byte[] keystream = rand.getSeed(20);
|
|
||||||
System.arraycopy(keystream, 0, encrypted, 0, 20);
|
|
||||||
int count = 0;
|
|
||||||
while (count < k.length) {
|
|
||||||
sha.reset();
|
|
||||||
sha.update(passwd);
|
|
||||||
sha.update(keystream);
|
|
||||||
sha.digest(keystream, 0, keystream.length);
|
|
||||||
for (int i = 0; i < keystream.length && count < k.length; i++) {
|
|
||||||
encrypted[count + 20] = (byte) (keystream[i] ^ k[count]);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sha.reset();
|
|
||||||
sha.update(passwd);
|
|
||||||
sha.update(k);
|
|
||||||
sha.digest(encrypted, encrypted.length - 20, 20);
|
|
||||||
// 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this
|
|
||||||
// encryption algorithm.
|
|
||||||
return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1",
|
|
||||||
encrypted).getEncoded();
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new KeyStoreException(x.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] charsToBytes(char[] passwd) {
|
|
||||||
byte[] buf = new byte[passwd.length * 2];
|
|
||||||
for (int i = 0, j = 0; i < passwd.length; i++) {
|
|
||||||
buf[j++] = (byte) (passwd[i] >>> 8);
|
|
||||||
buf[j++] = (byte) passwd[i];
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
|
|
||||||
import java.security.KeyStore;
|
|
||||||
|
|
||||||
public class JksKeyStore extends KeyStore {
|
|
||||||
|
|
||||||
public JksKeyStore() {
|
|
||||||
super(new JKS(), KeyStoreFileManager.getProvider(), "jks");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
public class KeyNameConflictException extends Exception {
|
|
||||||
}
|
|
@ -1,285 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public class KeyStoreFileManager {
|
|
||||||
|
|
||||||
static Provider provider = new BouncyCastleProvider();
|
|
||||||
|
|
||||||
public static Provider getProvider() {
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setProvider(Provider provider) {
|
|
||||||
if (KeyStoreFileManager.provider != null) Security.removeProvider(KeyStoreFileManager.provider.getName());
|
|
||||||
KeyStoreFileManager.provider = provider;
|
|
||||||
Security.addProvider(provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
static LoggerInterface logger = LoggerManager.getLogger(KeyStoreFileManager.class.getName());
|
|
||||||
|
|
||||||
static {
|
|
||||||
// Add the bouncycastle version of the BC provider so that the implementation classes returned
|
|
||||||
// from the keystore are all from the bouncycastle libs.
|
|
||||||
Security.addProvider(getProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static KeyStore loadKeyStore(String keystorePath, String encodedPassword)
|
|
||||||
throws Exception {
|
|
||||||
char password[] = null;
|
|
||||||
try {
|
|
||||||
if (encodedPassword != null) {
|
|
||||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
|
||||||
}
|
|
||||||
return loadKeyStore(keystorePath, password);
|
|
||||||
} finally {
|
|
||||||
if (password != null) PasswordObfuscator.flush(password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyStore createKeyStore(String keystorePath, char[] password)
|
|
||||||
throws Exception {
|
|
||||||
KeyStore ks = null;
|
|
||||||
if (keystorePath.toLowerCase(Locale.ENGLISH).endsWith(".bks")) {
|
|
||||||
ks = KeyStore.getInstance("bks", new BouncyCastleProvider());
|
|
||||||
} else ks = new JksKeyStore();
|
|
||||||
ks.load(null, password);
|
|
||||||
|
|
||||||
return ks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyStore loadKeyStore(String keystorePath, char[] password)
|
|
||||||
throws Exception {
|
|
||||||
KeyStore ks = null;
|
|
||||||
try {
|
|
||||||
ks = new JksKeyStore();
|
|
||||||
FileInputStream fis = new FileInputStream(keystorePath);
|
|
||||||
ks.load(fis, password);
|
|
||||||
fis.close();
|
|
||||||
return ks;
|
|
||||||
} catch (LoadKeystoreException x) {
|
|
||||||
// This type of exception is thrown when the keystore is a JKS keystore, but the file is malformed
|
|
||||||
// or the validity/password check failed. In this case don't bother to attempt loading it as a BKS keystore.
|
|
||||||
throw x;
|
|
||||||
} catch (Exception x) {
|
|
||||||
// logger.warning( x.getMessage(), x);
|
|
||||||
try {
|
|
||||||
ks = KeyStore.getInstance("bks", getProvider());
|
|
||||||
FileInputStream fis = new FileInputStream(keystorePath);
|
|
||||||
ks.load(fis, password);
|
|
||||||
fis.close();
|
|
||||||
return ks;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to load keystore: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeKeyStore(KeyStore ks, String keystorePath, String encodedPassword)
|
|
||||||
throws Exception {
|
|
||||||
char password[] = null;
|
|
||||||
try {
|
|
||||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
|
||||||
writeKeyStore(ks, keystorePath, password);
|
|
||||||
} finally {
|
|
||||||
if (password != null) PasswordObfuscator.flush(password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeKeyStore(KeyStore ks, String keystorePath, char[] password)
|
|
||||||
throws Exception {
|
|
||||||
|
|
||||||
File keystoreFile = new File(keystorePath);
|
|
||||||
try {
|
|
||||||
if (keystoreFile.exists()) {
|
|
||||||
// I've had some trouble saving new versions of the keystore file in which the file becomes empty/corrupt.
|
|
||||||
// Saving the new version to a new file and creating a backup of the old version.
|
|
||||||
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
|
|
||||||
FileOutputStream fos = new FileOutputStream(tmpFile);
|
|
||||||
ks.store(fos, password);
|
|
||||||
fos.flush();
|
|
||||||
fos.close();
|
|
||||||
/* create a backup of the previous version
|
|
||||||
int i = 1;
|
|
||||||
File backup = new File( keystorePath + "." + i + ".bak");
|
|
||||||
while (backup.exists()) {
|
|
||||||
i += 1;
|
|
||||||
backup = new File( keystorePath + "." + i + ".bak");
|
|
||||||
}
|
|
||||||
renameTo(keystoreFile, backup);
|
|
||||||
*/
|
|
||||||
renameTo(tmpFile, keystoreFile);
|
|
||||||
} else {
|
|
||||||
FileOutputStream fos = new FileOutputStream(keystorePath);
|
|
||||||
ks.store(fos, password);
|
|
||||||
fos.close();
|
|
||||||
}
|
|
||||||
} catch (Exception x) {
|
|
||||||
try {
|
|
||||||
File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
|
|
||||||
PrintWriter pw = new PrintWriter(new FileWriter(logfile));
|
|
||||||
x.printStackTrace(pw);
|
|
||||||
pw.flush();
|
|
||||||
pw.close();
|
|
||||||
} catch (Exception y) {
|
|
||||||
}
|
|
||||||
throw x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
|
|
||||||
if (destFile.exists() && destFile.isDirectory()) {
|
|
||||||
throw new IOException("Destination '" + destFile + "' exists but is a directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
FileInputStream input = new FileInputStream(srcFile);
|
|
||||||
try {
|
|
||||||
FileOutputStream output = new FileOutputStream(destFile);
|
|
||||||
try {
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
long count = 0;
|
|
||||||
int n = 0;
|
|
||||||
while (-1 != (n = input.read(buffer))) {
|
|
||||||
output.write(buffer, 0, n);
|
|
||||||
count += n;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
output.close();
|
|
||||||
} catch (IOException x) {
|
|
||||||
} // Ignore
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
input.close();
|
|
||||||
} catch (IOException x) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (srcFile.length() != destFile.length()) {
|
|
||||||
throw new IOException("Failed to copy full contents from '" +
|
|
||||||
srcFile + "' to '" + destFile + "'");
|
|
||||||
}
|
|
||||||
if (preserveFileDate) {
|
|
||||||
destFile.setLastModified(srcFile.lastModified());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void renameTo(File fromFile, File toFile)
|
|
||||||
throws IOException {
|
|
||||||
copyFile(fromFile, toFile, true);
|
|
||||||
if (!fromFile.delete()) throw new IOException("Failed to delete " + fromFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void deleteKey(String storePath, String storePass, String keyName)
|
|
||||||
throws Exception {
|
|
||||||
KeyStore ks = loadKeyStore(storePath, storePass);
|
|
||||||
ks.deleteEntry(keyName);
|
|
||||||
writeKeyStore(ks, storePath, storePass);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String renameKey(String keystorePath, String storePass, String oldKeyName, String newKeyName, String keyPass)
|
|
||||||
throws Exception {
|
|
||||||
char[] keyPw = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
|
||||||
if (ks instanceof JksKeyStore) newKeyName = newKeyName.toLowerCase(Locale.ENGLISH);
|
|
||||||
|
|
||||||
if (ks.containsAlias(newKeyName)) throw new KeyNameConflictException();
|
|
||||||
|
|
||||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, oldKeyName, keyPass);
|
|
||||||
Key key = ks.getKey(oldKeyName, keyPw);
|
|
||||||
Certificate cert = ks.getCertificate(oldKeyName);
|
|
||||||
|
|
||||||
ks.setKeyEntry(newKeyName, key, keyPw, new Certificate[]{cert});
|
|
||||||
ks.deleteEntry(oldKeyName);
|
|
||||||
|
|
||||||
writeKeyStore(ks, keystorePath, storePass);
|
|
||||||
return newKeyName;
|
|
||||||
} finally {
|
|
||||||
PasswordObfuscator.flush(keyPw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyStore.Entry getKeyEntry(String keystorePath, String storePass, String keyName, String keyPass)
|
|
||||||
throws Exception {
|
|
||||||
char[] keyPw = null;
|
|
||||||
KeyStore.PasswordProtection passwordProtection = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
|
||||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, keyPass);
|
|
||||||
passwordProtection = new KeyStore.PasswordProtection(keyPw);
|
|
||||||
return ks.getEntry(keyName, passwordProtection);
|
|
||||||
} finally {
|
|
||||||
if (keyPw != null) PasswordObfuscator.flush(keyPw);
|
|
||||||
if (passwordProtection != null) passwordProtection.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean containsKey(String keystorePath, String storePass, String keyName)
|
|
||||||
throws Exception {
|
|
||||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
|
||||||
return ks.containsAlias(keyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param keystorePath
|
|
||||||
* @param encodedPassword
|
|
||||||
* @throws Exception if the password is invalid
|
|
||||||
*/
|
|
||||||
public static void validateKeystorePassword(String keystorePath, String encodedPassword)
|
|
||||||
throws Exception {
|
|
||||||
char[] password = null;
|
|
||||||
try {
|
|
||||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, encodedPassword);
|
|
||||||
} finally {
|
|
||||||
if (password != null) PasswordObfuscator.flush(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param keystorePath
|
|
||||||
* @param keyName
|
|
||||||
* @param encodedPassword
|
|
||||||
* @throws java.security.UnrecoverableKeyException if the password is invalid
|
|
||||||
*/
|
|
||||||
public static void validateKeyPassword(String keystorePath, String keyName, String encodedPassword)
|
|
||||||
throws Exception {
|
|
||||||
char[] password = null;
|
|
||||||
try {
|
|
||||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, (char[]) null);
|
|
||||||
password = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, encodedPassword);
|
|
||||||
ks.getKey(keyName, password);
|
|
||||||
} finally {
|
|
||||||
if (password != null) PasswordObfuscator.flush(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore.
|
|
||||||
*/
|
|
||||||
public class LoadKeystoreException extends IOException {
|
|
||||||
|
|
||||||
public LoadKeystoreException() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoadKeystoreException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoadKeystoreException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoadKeystoreException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
import kellinwood.security.zipsigner.Base64;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
|
|
||||||
public class PasswordObfuscator {
|
|
||||||
|
|
||||||
private static PasswordObfuscator instance = null;
|
|
||||||
|
|
||||||
static final String x = "harold-and-maude";
|
|
||||||
|
|
||||||
LoggerInterface logger;
|
|
||||||
SecretKeySpec skeySpec;
|
|
||||||
|
|
||||||
private PasswordObfuscator() {
|
|
||||||
logger = LoggerManager.getLogger(PasswordObfuscator.class.getName());
|
|
||||||
skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PasswordObfuscator getInstance() {
|
|
||||||
if (instance == null) instance = new PasswordObfuscator();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encodeKeystorePassword(String keystorePath, String password) {
|
|
||||||
return encode(keystorePath, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encodeKeystorePassword(String keystorePath, char[] password) {
|
|
||||||
return encode(keystorePath, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encodeAliasPassword(String keystorePath, String aliasName, String password) {
|
|
||||||
return encode(keystorePath + aliasName, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encodeAliasPassword(String keystorePath, String aliasName, char[] password) {
|
|
||||||
return encode(keystorePath + aliasName, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public char[] decodeKeystorePassword(String keystorePath, String password) {
|
|
||||||
return decode(keystorePath, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public char[] decodeAliasPassword(String keystorePath, String aliasName, String password) {
|
|
||||||
return decode(keystorePath + aliasName, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encode(String junk, String password) {
|
|
||||||
if (password == null) return null;
|
|
||||||
char[] c = password.toCharArray();
|
|
||||||
String result = encode(junk, c);
|
|
||||||
flush(c);
|
|
||||||
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) {
|
|
||||||
if (password == null) return null;
|
|
||||||
try {
|
|
||||||
// Instantiate the cipher
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
Writer w = new OutputStreamWriter(baos);
|
|
||||||
w.write(junk);
|
|
||||||
w.write(password);
|
|
||||||
w.flush();
|
|
||||||
byte[] encoded = cipher.doFinal(baos.toByteArray());
|
|
||||||
return Base64.encode(encoded);
|
|
||||||
} catch (Exception x) {
|
|
||||||
logger.error("Failed to obfuscate password", x);
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if (password == null) return null;
|
|
||||||
try {
|
|
||||||
// Instantiate the cipher
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
|
||||||
SecretKeySpec skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
|
|
||||||
byte[] bytes = cipher.doFinal(Base64.decode(password.getBytes()));
|
|
||||||
BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
|
|
||||||
char[] cb = new char[128];
|
|
||||||
int length = 0;
|
|
||||||
int numRead;
|
|
||||||
while ((numRead = r.read(cb, length, 128 - length)) != -1) {
|
|
||||||
length += numRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length <= junk.length()) return null;
|
|
||||||
|
|
||||||
char[] result = new char[length - junk.length()];
|
|
||||||
int j = 0;
|
|
||||||
for (int i = junk.length(); i < length; i++) {
|
|
||||||
result[j] = cb[i];
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
flush(cb);
|
|
||||||
return result;
|
|
||||||
|
|
||||||
} catch (Exception x) {
|
|
||||||
logger.error("Failed to decode password", x);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void flush(char[] charArray) {
|
|
||||||
if (charArray == null) return;
|
|
||||||
for (int i = 0; i < charArray.length; i++) {
|
|
||||||
charArray[i] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void flush(byte[] charArray) {
|
|
||||||
if (charArray == null) return;
|
|
||||||
for (int i = 0; i < charArray.length; i++) {
|
|
||||||
charArray[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
|
|
||||||
package kellinwood.security.zipsigner.optional;
|
|
||||||
|
|
||||||
import kellinwood.security.zipsigner.KeySet;
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
|
||||||
import org.bouncycastle.cms.CMSSignedData;
|
|
||||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
|
||||||
import org.bouncycastle.cms.CMSTypedData;
|
|
||||||
import org.bouncycastle.cms.SignerInfoGenerator;
|
|
||||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
|
||||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
|
||||||
import org.bouncycastle.util.Store;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class SignatureBlockGenerator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign the given content using the private and public keys from the keySet, and return the encoded CMS (PKCS#7) data.
|
|
||||||
* Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs.
|
|
||||||
*/
|
|
||||||
public static byte[] generate(KeySet keySet, byte[] content) {
|
|
||||||
try {
|
|
||||||
List certList = new ArrayList();
|
|
||||||
CMSTypedData msg = new CMSProcessableByteArray(content);
|
|
||||||
|
|
||||||
certList.add(keySet.getPublicKey());
|
|
||||||
|
|
||||||
Store certs = new JcaCertStore(certList);
|
|
||||||
|
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
|
||||||
|
|
||||||
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("BC");
|
|
||||||
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
|
|
||||||
|
|
||||||
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC");
|
|
||||||
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
|
|
||||||
|
|
||||||
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
|
|
||||||
jcaSignerInfoGeneratorBuilder.setDirectSignature(true);
|
|
||||||
SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.getPublicKey());
|
|
||||||
|
|
||||||
gen.addSignerInfoGenerator(signerInfoGenerator);
|
|
||||||
|
|
||||||
gen.addCertificates(certs);
|
|
||||||
|
|
||||||
CMSSignedData sigData = gen.generate(msg, false);
|
|
||||||
return sigData.toASN1Structure().getEncoded("DER");
|
|
||||||
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new RuntimeException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class CentralEnd {
|
|
||||||
public int signature = 0x06054b50; // end of central dir signature 4 bytes
|
|
||||||
public short numberThisDisk = 0; // number of this disk 2 bytes
|
|
||||||
public short centralStartDisk = 0; // number of the disk with the start of the central directory 2 bytes
|
|
||||||
public short numCentralEntries; // total number of entries in the central directory on this disk 2 bytes
|
|
||||||
public short totalCentralEntries; // total number of entries in the central directory 2 bytes
|
|
||||||
|
|
||||||
public int centralDirectorySize; // size of the central directory 4 bytes
|
|
||||||
public int centralStartOffset; // offset of start of central directory with respect to the starting disk number 4 bytes
|
|
||||||
public String fileComment; // .ZIP file comment (variable size)
|
|
||||||
|
|
||||||
private static LoggerInterface log;
|
|
||||||
|
|
||||||
public static CentralEnd read(ZipInput input) throws IOException {
|
|
||||||
|
|
||||||
int signature = input.readInt();
|
|
||||||
if (signature != 0x06054b50) {
|
|
||||||
// back up to the signature
|
|
||||||
input.seek(input.getFilePointer() - 4);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
CentralEnd entry = new CentralEnd();
|
|
||||||
|
|
||||||
entry.doRead(input);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LoggerInterface getLogger() {
|
|
||||||
if (log == null) log = LoggerManager.getLogger(CentralEnd.class.getName());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void doRead(ZipInput input) throws IOException {
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
numberThisDisk = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("This disk number: 0x%04x", numberThisDisk));
|
|
||||||
|
|
||||||
centralStartDisk = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Central dir start disk number: 0x%04x", centralStartDisk));
|
|
||||||
|
|
||||||
numCentralEntries = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Central entries on this disk: 0x%04x", numCentralEntries));
|
|
||||||
|
|
||||||
totalCentralEntries = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Total number of central entries: 0x%04x", totalCentralEntries));
|
|
||||||
|
|
||||||
centralDirectorySize = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Central directory size: 0x%08x", centralDirectorySize));
|
|
||||||
|
|
||||||
centralStartOffset = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Central directory offset: 0x%08x", centralStartOffset));
|
|
||||||
|
|
||||||
short zipFileCommentLen = input.readShort();
|
|
||||||
fileComment = input.readString(zipFileCommentLen);
|
|
||||||
if (debug) log.debug(".ZIP file comment: " + fileComment);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void write(ZipOutput output) throws IOException {
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
output.writeInt(signature);
|
|
||||||
output.writeShort(numberThisDisk);
|
|
||||||
output.writeShort(centralStartDisk);
|
|
||||||
output.writeShort(numCentralEntries);
|
|
||||||
output.writeShort(totalCentralEntries);
|
|
||||||
output.writeInt(centralDirectorySize);
|
|
||||||
output.writeInt(centralStartOffset);
|
|
||||||
output.writeShort((short) fileComment.length());
|
|
||||||
output.writeString(fileComment);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,632 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.SequenceInputStream;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.zip.CRC32;
|
|
||||||
import java.util.zip.Inflater;
|
|
||||||
import java.util.zip.InflaterInputStream;
|
|
||||||
|
|
||||||
public class ZioEntry implements Cloneable {
|
|
||||||
|
|
||||||
private ZipInput zipInput;
|
|
||||||
|
|
||||||
// public int signature = 0x02014b50;
|
|
||||||
private short versionMadeBy;
|
|
||||||
private short versionRequired;
|
|
||||||
private short generalPurposeBits;
|
|
||||||
private short compression;
|
|
||||||
private short modificationTime;
|
|
||||||
private short modificationDate;
|
|
||||||
private int crc32;
|
|
||||||
private int compressedSize;
|
|
||||||
private int size;
|
|
||||||
private String filename;
|
|
||||||
private byte[] extraData;
|
|
||||||
private short numAlignBytes = 0;
|
|
||||||
private String fileComment;
|
|
||||||
private short diskNumberStart;
|
|
||||||
private short internalAttributes;
|
|
||||||
private int externalAttributes;
|
|
||||||
|
|
||||||
private int localHeaderOffset;
|
|
||||||
private long dataPosition = -1;
|
|
||||||
private byte[] data = null;
|
|
||||||
private ZioEntryOutputStream entryOut = null;
|
|
||||||
|
|
||||||
|
|
||||||
private static byte[] alignBytes = new byte[4];
|
|
||||||
|
|
||||||
private static LoggerInterface log;
|
|
||||||
|
|
||||||
public ZioEntry(ZipInput input) {
|
|
||||||
zipInput = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LoggerInterface getLogger() {
|
|
||||||
if (log == null) log = LoggerManager.getLogger(ZioEntry.class.getName());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZioEntry(String name) {
|
|
||||||
filename = name;
|
|
||||||
fileComment = "";
|
|
||||||
compression = 8;
|
|
||||||
extraData = new byte[0];
|
|
||||||
setTime(System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ZioEntry(String name, String sourceDataFile)
|
|
||||||
throws IOException {
|
|
||||||
zipInput = new ZipInput(sourceDataFile);
|
|
||||||
filename = name;
|
|
||||||
fileComment = "";
|
|
||||||
this.compression = 0;
|
|
||||||
this.size = (int) zipInput.getFileLength();
|
|
||||||
this.compressedSize = this.size;
|
|
||||||
|
|
||||||
if (getLogger().isDebugEnabled())
|
|
||||||
getLogger().debug(String.format(Locale.ENGLISH, "Computing CRC for %s, size=%d", sourceDataFile, size));
|
|
||||||
|
|
||||||
// compute CRC
|
|
||||||
CRC32 crc = new CRC32();
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8096];
|
|
||||||
|
|
||||||
int numRead = 0;
|
|
||||||
while (numRead != size) {
|
|
||||||
int count = zipInput.read(buffer, 0, Math.min(buffer.length, (this.size - numRead)));
|
|
||||||
if (count > 0) {
|
|
||||||
crc.update(buffer, 0, count);
|
|
||||||
numRead += count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.crc32 = (int) crc.getValue();
|
|
||||||
|
|
||||||
zipInput.seek(0);
|
|
||||||
this.dataPosition = 0;
|
|
||||||
extraData = new byte[0];
|
|
||||||
setTime(new File(sourceDataFile).lastModified());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ZioEntry(String name, String sourceDataFile, short compression, int crc32, int compressedSize, int size)
|
|
||||||
throws IOException {
|
|
||||||
zipInput = new ZipInput(sourceDataFile);
|
|
||||||
filename = name;
|
|
||||||
fileComment = "";
|
|
||||||
this.compression = compression;
|
|
||||||
this.crc32 = crc32;
|
|
||||||
this.compressedSize = compressedSize;
|
|
||||||
this.size = size;
|
|
||||||
this.dataPosition = 0;
|
|
||||||
extraData = new byte[0];
|
|
||||||
setTime(new File(sourceDataFile).lastModified());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a copy with a new name
|
|
||||||
public ZioEntry getClonedEntry(String newName) {
|
|
||||||
|
|
||||||
ZioEntry clone;
|
|
||||||
try {
|
|
||||||
clone = (ZioEntry) this.clone();
|
|
||||||
} catch (CloneNotSupportedException e) {
|
|
||||||
throw new IllegalStateException("clone() failed!");
|
|
||||||
}
|
|
||||||
clone.setName(newName);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void readLocalHeader() throws IOException {
|
|
||||||
ZipInput input = zipInput;
|
|
||||||
int tmp;
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
input.seek(localHeaderOffset);
|
|
||||||
|
|
||||||
if (debug) getLogger().debug(String.format("FILE POSITION: 0x%08x", input.getFilePointer()));
|
|
||||||
|
|
||||||
// 0 4 Local file header signature = 0x04034b50
|
|
||||||
int signature = input.readInt();
|
|
||||||
if (signature != 0x04034b50) {
|
|
||||||
throw new IllegalStateException(String.format("Local header not found at pos=0x%08x, file=%s", input.getFilePointer(), filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is usually called just before the data read, so
|
|
||||||
// its only purpose currently is to position the file pointer
|
|
||||||
// for the data read. The entry's attributes might also have
|
|
||||||
// been changed since the central dir entry was read (e.g.,
|
|
||||||
// filename), so throw away the values here.
|
|
||||||
|
|
||||||
int tmpInt;
|
|
||||||
short tmpShort;
|
|
||||||
|
|
||||||
// 4 2 Version needed to extract (minimum)
|
|
||||||
/* versionRequired */
|
|
||||||
tmpShort = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Version required: 0x%04x", tmpShort /*versionRequired*/));
|
|
||||||
|
|
||||||
// 6 2 General purpose bit flag
|
|
||||||
/* generalPurposeBits */
|
|
||||||
tmpShort = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("General purpose bits: 0x%04x", tmpShort /* generalPurposeBits */));
|
|
||||||
|
|
||||||
// 8 2 Compression method
|
|
||||||
/* compression */
|
|
||||||
tmpShort = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Compression: 0x%04x", tmpShort /* compression */));
|
|
||||||
|
|
||||||
// 10 2 File last modification time
|
|
||||||
/* modificationTime */
|
|
||||||
tmpShort = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Modification time: 0x%04x", tmpShort /* modificationTime */));
|
|
||||||
|
|
||||||
// 12 2 File last modification date
|
|
||||||
/* modificationDate */
|
|
||||||
tmpShort = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Modification date: 0x%04x", tmpShort /* modificationDate */));
|
|
||||||
|
|
||||||
// 14 4 CRC-32
|
|
||||||
/* crc32 */
|
|
||||||
tmpInt = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("CRC-32: 0x%04x", tmpInt /*crc32*/));
|
|
||||||
|
|
||||||
// 18 4 Compressed size
|
|
||||||
/* compressedSize*/
|
|
||||||
tmpInt = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Compressed size: 0x%04x", tmpInt /*compressedSize*/));
|
|
||||||
|
|
||||||
// 22 4 Uncompressed size
|
|
||||||
/* size */
|
|
||||||
tmpInt = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Size: 0x%04x", tmpInt /*size*/));
|
|
||||||
|
|
||||||
// 26 2 File name length (n)
|
|
||||||
short fileNameLen = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
|
|
||||||
|
|
||||||
// 28 2 Extra field length (m)
|
|
||||||
short extraLen = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
|
|
||||||
|
|
||||||
// 30 n File name
|
|
||||||
String filename = input.readString(fileNameLen);
|
|
||||||
if (debug) log.debug("Filename: " + filename);
|
|
||||||
|
|
||||||
// Extra data
|
|
||||||
byte[] extra = input.readBytes(extraLen);
|
|
||||||
|
|
||||||
// Record the file position of this entry's data.
|
|
||||||
dataPosition = input.getFilePointer();
|
|
||||||
if (debug) log.debug(String.format("Data position: 0x%08x", dataPosition));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeLocalEntry(ZipOutput output) throws IOException {
|
|
||||||
if (data == null && dataPosition < 0 && zipInput != null) {
|
|
||||||
readLocalHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
localHeaderOffset = (int) output.getFilePointer();
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
getLogger().debug(String.format("Writing local header at 0x%08x - %s", localHeaderOffset, filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entryOut != null) {
|
|
||||||
entryOut.close();
|
|
||||||
size = entryOut.getSize();
|
|
||||||
data = ((ByteArrayOutputStream) entryOut.getWrappedStream()).toByteArray();
|
|
||||||
compressedSize = data.length;
|
|
||||||
crc32 = entryOut.getCRC();
|
|
||||||
}
|
|
||||||
|
|
||||||
output.writeInt(0x04034b50);
|
|
||||||
output.writeShort(versionRequired);
|
|
||||||
output.writeShort(generalPurposeBits);
|
|
||||||
output.writeShort(compression);
|
|
||||||
output.writeShort(modificationTime);
|
|
||||||
output.writeShort(modificationDate);
|
|
||||||
output.writeInt(crc32);
|
|
||||||
output.writeInt(compressedSize);
|
|
||||||
output.writeInt(size);
|
|
||||||
output.writeShort((short) filename.length());
|
|
||||||
|
|
||||||
numAlignBytes = 0;
|
|
||||||
|
|
||||||
// Zipalign if the file is uncompressed, i.e., "Stored", and file size is not zero.
|
|
||||||
if (compression == 0) {
|
|
||||||
|
|
||||||
long dataPos = output.getFilePointer() + // current position
|
|
||||||
2 + // plus size of extra data length
|
|
||||||
filename.length() + // plus filename
|
|
||||||
extraData.length; // plus extra data
|
|
||||||
|
|
||||||
short dataPosMod4 = (short) (dataPos % 4);
|
|
||||||
|
|
||||||
if (dataPosMod4 > 0) {
|
|
||||||
numAlignBytes = (short) (4 - dataPosMod4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 28 2 Extra field length (m)
|
|
||||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
|
||||||
|
|
||||||
// 30 n File name
|
|
||||||
output.writeString(filename);
|
|
||||||
|
|
||||||
// Extra data
|
|
||||||
output.writeBytes(extraData);
|
|
||||||
|
|
||||||
// Zipalign bytes
|
|
||||||
if (numAlignBytes > 0) {
|
|
||||||
output.writeBytes(alignBytes, 0, numAlignBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Data position 0x%08x", output.getFilePointer()));
|
|
||||||
if (data != null) {
|
|
||||||
output.writeBytes(data);
|
|
||||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Wrote %d bytes", data.length));
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if (debug) getLogger().debug(String.format("Seeking to position 0x%08x", dataPosition));
|
|
||||||
zipInput.seek(dataPosition);
|
|
||||||
|
|
||||||
int bufferSize = Math.min(compressedSize, 8096);
|
|
||||||
byte[] buffer = new byte[bufferSize];
|
|
||||||
long totalCount = 0;
|
|
||||||
|
|
||||||
while (totalCount != compressedSize) {
|
|
||||||
int numRead = zipInput.in.read(buffer, 0, (int) Math.min(compressedSize - totalCount, bufferSize));
|
|
||||||
if (numRead > 0) {
|
|
||||||
output.writeBytes(buffer, 0, numRead);
|
|
||||||
if (debug) getLogger().debug(String.format(Locale.ENGLISH, "Wrote %d bytes", numRead));
|
|
||||||
totalCount += numRead;
|
|
||||||
} else
|
|
||||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "EOF reached while copying %s with %d bytes left to go", filename, compressedSize - totalCount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ZioEntry read(ZipInput input) throws IOException {
|
|
||||||
|
|
||||||
// 0 4 Central directory header signature = 0x02014b50
|
|
||||||
int signature = input.readInt();
|
|
||||||
if (signature != 0x02014b50) {
|
|
||||||
// back up to the signature
|
|
||||||
input.seek(input.getFilePointer() - 4);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ZioEntry entry = new ZioEntry(input);
|
|
||||||
|
|
||||||
entry.doRead(input);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doRead(ZipInput input) throws IOException {
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
// 4 2 Version needed to extract (minimum)
|
|
||||||
versionMadeBy = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Version made by: 0x%04x", versionMadeBy));
|
|
||||||
|
|
||||||
// 4 2 Version required
|
|
||||||
versionRequired = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Version required: 0x%04x", versionRequired));
|
|
||||||
|
|
||||||
// 6 2 General purpose bit flag
|
|
||||||
generalPurposeBits = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("General purpose bits: 0x%04x", generalPurposeBits));
|
|
||||||
// Bits 1, 2, 3, and 11 are allowed to be set (first bit is bit zero). Any others are a problem.
|
|
||||||
if ((generalPurposeBits & 0xF7F1) != 0x0000) {
|
|
||||||
throw new IllegalStateException("Can't handle general purpose bits == " + String.format("0x%04x", generalPurposeBits));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8 2 Compression method
|
|
||||||
compression = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Compression: 0x%04x", compression));
|
|
||||||
|
|
||||||
// 10 2 File last modification time
|
|
||||||
modificationTime = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Modification time: 0x%04x", modificationTime));
|
|
||||||
|
|
||||||
// 12 2 File last modification date
|
|
||||||
modificationDate = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Modification date: 0x%04x", modificationDate));
|
|
||||||
|
|
||||||
// 14 4 CRC-32
|
|
||||||
crc32 = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("CRC-32: 0x%04x", crc32));
|
|
||||||
|
|
||||||
// 18 4 Compressed size
|
|
||||||
compressedSize = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Compressed size: 0x%04x", compressedSize));
|
|
||||||
|
|
||||||
// 22 4 Uncompressed size
|
|
||||||
size = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Size: 0x%04x", size));
|
|
||||||
|
|
||||||
// 26 2 File name length (n)
|
|
||||||
short fileNameLen = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("File name length: 0x%04x", fileNameLen));
|
|
||||||
|
|
||||||
// 28 2 Extra field length (m)
|
|
||||||
short extraLen = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Extra length: 0x%04x", extraLen));
|
|
||||||
|
|
||||||
short fileCommentLen = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("File comment length: 0x%04x", fileCommentLen));
|
|
||||||
|
|
||||||
diskNumberStart = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Disk number start: 0x%04x", diskNumberStart));
|
|
||||||
|
|
||||||
internalAttributes = input.readShort();
|
|
||||||
if (debug) log.debug(String.format("Internal attributes: 0x%04x", internalAttributes));
|
|
||||||
|
|
||||||
externalAttributes = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("External attributes: 0x%08x", externalAttributes));
|
|
||||||
|
|
||||||
localHeaderOffset = input.readInt();
|
|
||||||
if (debug) log.debug(String.format("Local header offset: 0x%08x", localHeaderOffset));
|
|
||||||
|
|
||||||
// 30 n File name
|
|
||||||
filename = input.readString(fileNameLen);
|
|
||||||
if (debug) log.debug("Filename: " + filename);
|
|
||||||
|
|
||||||
extraData = input.readBytes(extraLen);
|
|
||||||
|
|
||||||
fileComment = input.readString(fileCommentLen);
|
|
||||||
if (debug) log.debug("File comment: " + fileComment);
|
|
||||||
|
|
||||||
generalPurposeBits = (short) (generalPurposeBits & 0x0800); // Don't write a data descriptor, preserve UTF-8 encoded filename bit
|
|
||||||
|
|
||||||
// Don't write zero-length entries with compression.
|
|
||||||
if (size == 0) {
|
|
||||||
compressedSize = 0;
|
|
||||||
compression = 0;
|
|
||||||
crc32 = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the entry's data.
|
|
||||||
*/
|
|
||||||
public byte[] getData() throws IOException {
|
|
||||||
if (data != null) return data;
|
|
||||||
|
|
||||||
byte[] tmpdata = new byte[size];
|
|
||||||
|
|
||||||
InputStream din = getInputStream();
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
while (count != size) {
|
|
||||||
int numRead = din.read(tmpdata, count, size - count);
|
|
||||||
if (numRead < 0)
|
|
||||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "Read failed, expecting %d bytes, got %d instead", size, count));
|
|
||||||
count += numRead;
|
|
||||||
}
|
|
||||||
return tmpdata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an input stream for reading the entry's data.
|
|
||||||
public InputStream getInputStream() throws IOException {
|
|
||||||
return getInputStream(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an input stream for reading the entry's data.
|
|
||||||
public InputStream getInputStream(OutputStream monitorStream) throws IOException {
|
|
||||||
|
|
||||||
if (entryOut != null) {
|
|
||||||
entryOut.close();
|
|
||||||
size = entryOut.getSize();
|
|
||||||
data = ((ByteArrayOutputStream) entryOut.getWrappedStream()).toByteArray();
|
|
||||||
compressedSize = data.length;
|
|
||||||
crc32 = entryOut.getCRC();
|
|
||||||
entryOut = null;
|
|
||||||
InputStream rawis = new ByteArrayInputStream(data);
|
|
||||||
if (compression == 0) return rawis;
|
|
||||||
else {
|
|
||||||
// Hacky, inflate using a sequence of input streams that returns 1 byte more than the actual length of the data.
|
|
||||||
// This extra dummy byte is required by InflaterInputStream when the data doesn't have the header and crc fields (as it is in zip files).
|
|
||||||
return new InflaterInputStream(new SequenceInputStream(rawis, new ByteArrayInputStream(new byte[1])), new Inflater(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ZioEntryInputStream dataStream;
|
|
||||||
dataStream = new ZioEntryInputStream(this);
|
|
||||||
if (monitorStream != null) dataStream.setMonitorStream(monitorStream);
|
|
||||||
if (compression != 0) {
|
|
||||||
// Note: When using nowrap=true with Inflater it is also necessary to provide
|
|
||||||
// an extra "dummy" byte as input. This is required by the ZLIB native library
|
|
||||||
// in order to support certain optimizations.
|
|
||||||
dataStream.setReturnDummyByte(true);
|
|
||||||
return new InflaterInputStream(dataStream, new Inflater(true));
|
|
||||||
} else return dataStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an output stream for writing an entry's data.
|
|
||||||
public OutputStream getOutputStream() {
|
|
||||||
entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream());
|
|
||||||
return entryOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void write(ZipOutput output) throws IOException {
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
|
|
||||||
|
|
||||||
output.writeInt(0x02014b50);
|
|
||||||
output.writeShort(versionMadeBy);
|
|
||||||
output.writeShort(versionRequired);
|
|
||||||
output.writeShort(generalPurposeBits);
|
|
||||||
output.writeShort(compression);
|
|
||||||
output.writeShort(modificationTime);
|
|
||||||
output.writeShort(modificationDate);
|
|
||||||
output.writeInt(crc32);
|
|
||||||
output.writeInt(compressedSize);
|
|
||||||
output.writeInt(size);
|
|
||||||
output.writeShort((short) filename.length());
|
|
||||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
|
||||||
output.writeShort((short) fileComment.length());
|
|
||||||
output.writeShort(diskNumberStart);
|
|
||||||
output.writeShort(internalAttributes);
|
|
||||||
output.writeInt(externalAttributes);
|
|
||||||
output.writeInt(localHeaderOffset);
|
|
||||||
|
|
||||||
output.writeString(filename);
|
|
||||||
output.writeBytes(extraData);
|
|
||||||
if (numAlignBytes > 0) output.writeBytes(alignBytes, 0, numAlignBytes);
|
|
||||||
output.writeString(fileComment);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns timetamp in Java format
|
|
||||||
*/
|
|
||||||
public long getTime() {
|
|
||||||
int year = (int) (((modificationDate >> 9) & 0x007f) + 80);
|
|
||||||
int month = (int) (((modificationDate >> 5) & 0x000f) - 1);
|
|
||||||
int day = (int) (modificationDate & 0x001f);
|
|
||||||
int hour = (int) ((modificationTime >> 11) & 0x001f);
|
|
||||||
int minute = (int) ((modificationTime >> 5) & 0x003f);
|
|
||||||
int seconds = (int) ((modificationTime << 1) & 0x003e);
|
|
||||||
Date d = new Date(year, month, day, hour, minute, seconds);
|
|
||||||
return d.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the file timestamp (using a Java time value).
|
|
||||||
*/
|
|
||||||
public void setTime(long time) {
|
|
||||||
Date d = new Date(time);
|
|
||||||
long dtime;
|
|
||||||
int year = d.getYear() + 1900;
|
|
||||||
if (year < 1980) {
|
|
||||||
dtime = (1 << 21) | (1 << 16);
|
|
||||||
} else {
|
|
||||||
dtime = (year - 1980) << 25 | (d.getMonth() + 1) << 21 |
|
|
||||||
d.getDate() << 16 | d.getHours() << 11 | d.getMinutes() << 5 |
|
|
||||||
d.getSeconds() >> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
modificationDate = (short) (dtime >> 16);
|
|
||||||
modificationTime = (short) (dtime & 0xFFFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDirectory() {
|
|
||||||
return filename.endsWith("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String filename) {
|
|
||||||
this.filename = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use 0 (STORED), or 8 (DEFLATE).
|
|
||||||
*/
|
|
||||||
public void setCompression(int compression) {
|
|
||||||
this.compression = (short) compression;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getVersionMadeBy() {
|
|
||||||
return versionMadeBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getVersionRequired() {
|
|
||||||
return versionRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getGeneralPurposeBits() {
|
|
||||||
return generalPurposeBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getCompression() {
|
|
||||||
return compression;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCrc32() {
|
|
||||||
return crc32;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCompressedSize() {
|
|
||||||
return compressedSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getExtraData() {
|
|
||||||
return extraData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileComment() {
|
|
||||||
return fileComment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getDiskNumberStart() {
|
|
||||||
return diskNumberStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getInternalAttributes() {
|
|
||||||
return internalAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getExternalAttributes() {
|
|
||||||
return externalAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLocalHeaderOffset() {
|
|
||||||
return localHeaderOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDataPosition() {
|
|
||||||
return dataPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZioEntryOutputStream getEntryOut() {
|
|
||||||
return entryOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZipInput getZipInput() {
|
|
||||||
return zipInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Input stream used to read just the data from a zip file entry.
|
|
||||||
*/
|
|
||||||
public class ZioEntryInputStream extends InputStream {
|
|
||||||
|
|
||||||
RandomAccessFile raf;
|
|
||||||
int size;
|
|
||||||
int offset;
|
|
||||||
LoggerInterface log;
|
|
||||||
boolean debug;
|
|
||||||
boolean returnDummyByte = false;
|
|
||||||
OutputStream monitor = null;
|
|
||||||
|
|
||||||
public ZioEntryInputStream(ZioEntry entry) throws IOException {
|
|
||||||
|
|
||||||
log = LoggerManager.getLogger(this.getClass().getName());
|
|
||||||
debug = log.isDebugEnabled();
|
|
||||||
offset = 0;
|
|
||||||
size = entry.getCompressedSize();
|
|
||||||
raf = entry.getZipInput().in;
|
|
||||||
long dpos = entry.getDataPosition();
|
|
||||||
if (dpos >= 0) {
|
|
||||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Seeking to %d", entry.getDataPosition()));
|
|
||||||
raf.seek(entry.getDataPosition());
|
|
||||||
} else {
|
|
||||||
// seeks to, then reads, the local header, causing the
|
|
||||||
// file pointer to be positioned at the start of the data.
|
|
||||||
entry.readLocalHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReturnDummyByte(boolean returnExtraByte) {
|
|
||||||
returnDummyByte = returnExtraByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debugging, if the monitor is set we write all data read to the monitor.
|
|
||||||
public void setMonitorStream(OutputStream monitorStream) {
|
|
||||||
monitor = monitorStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean markSupported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() throws IOException {
|
|
||||||
int available = size - offset;
|
|
||||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Available = %d", available));
|
|
||||||
if (available == 0 && returnDummyByte) return 1;
|
|
||||||
else return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if ((size - offset) == 0) {
|
|
||||||
if (returnDummyByte) {
|
|
||||||
returnDummyByte = false;
|
|
||||||
return 0;
|
|
||||||
} else return -1;
|
|
||||||
}
|
|
||||||
int b = raf.read();
|
|
||||||
if (b >= 0) {
|
|
||||||
if (monitor != null) monitor.write(b);
|
|
||||||
if (debug) log.debug("Read 1 byte");
|
|
||||||
offset += 1;
|
|
||||||
} else if (debug) log.debug("Read 0 bytes");
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
return readBytes(b, off, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readBytes(byte[] b, int off, int len) throws IOException {
|
|
||||||
if ((size - offset) == 0) {
|
|
||||||
if (returnDummyByte) {
|
|
||||||
returnDummyByte = false;
|
|
||||||
b[off] = 0;
|
|
||||||
return 1;
|
|
||||||
} else return -1;
|
|
||||||
}
|
|
||||||
int numToRead = Math.min(len, available());
|
|
||||||
int numRead = raf.read(b, off, numToRead);
|
|
||||||
if (numRead > 0) {
|
|
||||||
if (monitor != null) monitor.write(b, off, numRead);
|
|
||||||
offset += numRead;
|
|
||||||
}
|
|
||||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Read %d bytes for read(b,%d,%d)", numRead, off, len));
|
|
||||||
return numRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return readBytes(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long skip(long n) throws IOException {
|
|
||||||
long numToSkip = Math.min(n, available());
|
|
||||||
raf.seek(raf.getFilePointer() + numToSkip);
|
|
||||||
if (debug) log.debug(String.format(Locale.ENGLISH, "Skipped %d bytes", numToSkip));
|
|
||||||
return numToSkip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.zip.CRC32;
|
|
||||||
import java.util.zip.Deflater;
|
|
||||||
import java.util.zip.DeflaterOutputStream;
|
|
||||||
|
|
||||||
public class ZioEntryOutputStream extends OutputStream {
|
|
||||||
int size = 0; // tracks uncompressed size of data
|
|
||||||
CRC32 crc = new CRC32();
|
|
||||||
int crcValue = 0;
|
|
||||||
OutputStream wrapped;
|
|
||||||
OutputStream downstream;
|
|
||||||
Deflater deflater;
|
|
||||||
|
|
||||||
public ZioEntryOutputStream(int compression, OutputStream wrapped) {
|
|
||||||
this.wrapped = wrapped;
|
|
||||||
if (compression != 0) {
|
|
||||||
deflater = new Deflater(Deflater.BEST_COMPRESSION, true);
|
|
||||||
downstream = new DeflaterOutputStream(wrapped, deflater);
|
|
||||||
} else {
|
|
||||||
downstream = wrapped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() throws IOException {
|
|
||||||
downstream.flush();
|
|
||||||
downstream.close();
|
|
||||||
crcValue = (int) crc.getValue();
|
|
||||||
if (deflater != null) {
|
|
||||||
deflater.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCRC() {
|
|
||||||
return crcValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void flush() throws IOException {
|
|
||||||
downstream.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(byte[] b) throws IOException {
|
|
||||||
downstream.write(b);
|
|
||||||
crc.update(b);
|
|
||||||
size += b.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(byte[] b, int off, int len) throws IOException {
|
|
||||||
downstream.write(b, off, len);
|
|
||||||
crc.update(b, off, len);
|
|
||||||
size += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
downstream.write(b);
|
|
||||||
crc.update(b);
|
|
||||||
size += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutputStream getWrappedStream() {
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class ZipInput implements Closeable {
|
|
||||||
static LoggerInterface log;
|
|
||||||
|
|
||||||
public String inputFilename;
|
|
||||||
RandomAccessFile in = null;
|
|
||||||
long fileLength;
|
|
||||||
int scanIterations = 0;
|
|
||||||
|
|
||||||
Map<String, ZioEntry> zioEntries = new LinkedHashMap<String, ZioEntry>();
|
|
||||||
CentralEnd centralEnd;
|
|
||||||
Manifest manifest;
|
|
||||||
|
|
||||||
public ZipInput(String filename) throws IOException {
|
|
||||||
this.inputFilename = filename;
|
|
||||||
in = new RandomAccessFile(new File(inputFilename), "r");
|
|
||||||
fileLength = in.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoggerInterface getLogger() {
|
|
||||||
if (log == null) log = LoggerManager.getLogger(ZipInput.class.getName());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename() {
|
|
||||||
return inputFilename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getFileLength() {
|
|
||||||
return fileLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ZipInput read(String filename) throws IOException {
|
|
||||||
ZipInput zipInput = new ZipInput(filename);
|
|
||||||
zipInput.doRead();
|
|
||||||
return zipInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ZioEntry getEntry(String filename) {
|
|
||||||
return zioEntries.get(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, ZioEntry> getEntries() {
|
|
||||||
return zioEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the names of immediate children in the directory with the given name.
|
|
||||||
* The path value must end with a "/" character. Use a value of "/"
|
|
||||||
* to get the root entries.
|
|
||||||
*/
|
|
||||||
public Collection<String> list(String path) {
|
|
||||||
if (!path.endsWith("/")) throw new IllegalArgumentException("Invalid path -- does not end with '/'");
|
|
||||||
|
|
||||||
if (path.startsWith("/")) path = path.substring(1);
|
|
||||||
|
|
||||||
Pattern p = Pattern.compile(String.format("^%s([^/]+/?).*", path));
|
|
||||||
|
|
||||||
Set<String> names = new TreeSet<String>();
|
|
||||||
|
|
||||||
for (String name : zioEntries.keySet()) {
|
|
||||||
Matcher m = p.matcher(name);
|
|
||||||
if (m.matches()) names.add(m.group(1));
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manifest getManifest() throws IOException {
|
|
||||||
if (manifest == null) {
|
|
||||||
ZioEntry e = zioEntries.get("META-INF/MANIFEST.MF");
|
|
||||||
if (e != null) {
|
|
||||||
manifest = new Manifest(e.getInputStream());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan the end of the file for the end of central directory record (EOCDR).
|
|
||||||
* Returns the file offset of the EOCD signature. The size parameter is an
|
|
||||||
* initial buffer size (e.g., 256).
|
|
||||||
*/
|
|
||||||
public long scanForEOCDR(int size) throws IOException {
|
|
||||||
if (size > fileLength || size > 65536)
|
|
||||||
throw new IllegalStateException("End of central directory not found in " + inputFilename);
|
|
||||||
|
|
||||||
int scanSize = (int) Math.min(fileLength, size);
|
|
||||||
|
|
||||||
byte[] scanBuf = new byte[scanSize];
|
|
||||||
|
|
||||||
in.seek(fileLength - scanSize);
|
|
||||||
|
|
||||||
in.readFully(scanBuf);
|
|
||||||
|
|
||||||
for (int i = scanSize - 22; i >= 0; i--) {
|
|
||||||
scanIterations += 1;
|
|
||||||
if (scanBuf[i] == 0x50 && scanBuf[i + 1] == 0x4b && scanBuf[i + 2] == 0x05 && scanBuf[i + 3] == 0x06) {
|
|
||||||
return fileLength - scanSize + i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scanForEOCDR(size * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void doRead() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
long posEOCDR = scanForEOCDR(256);
|
|
||||||
in.seek(posEOCDR);
|
|
||||||
centralEnd = CentralEnd.read(this);
|
|
||||||
|
|
||||||
boolean debug = getLogger().isDebugEnabled();
|
|
||||||
if (debug) {
|
|
||||||
getLogger().debug(String.format(Locale.ENGLISH, "EOCD found in %d iterations", scanIterations));
|
|
||||||
getLogger().debug(String.format(Locale.ENGLISH, "Directory entries=%d, size=%d, offset=%d/0x%08x", centralEnd.totalCentralEntries,
|
|
||||||
centralEnd.centralDirectorySize, centralEnd.centralStartOffset, centralEnd.centralStartOffset));
|
|
||||||
|
|
||||||
ZipListingHelper.listHeader(getLogger());
|
|
||||||
}
|
|
||||||
|
|
||||||
in.seek(centralEnd.centralStartOffset);
|
|
||||||
|
|
||||||
for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
|
|
||||||
ZioEntry entry = ZioEntry.read(this);
|
|
||||||
zioEntries.put(entry.getName(), entry);
|
|
||||||
if (debug) ZipListingHelper.listEntry(getLogger(), entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Throwable t) {
|
|
||||||
t.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
if (in != null) try {
|
|
||||||
in.close();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getFilePointer() throws IOException {
|
|
||||||
return in.getFilePointer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void seek(long position) throws IOException {
|
|
||||||
in.seek(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte readByte() throws IOException {
|
|
||||||
return in.readByte();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int readInt() throws IOException {
|
|
||||||
int result = 0;
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
result |= (in.readUnsignedByte() << (8 * i));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short readShort() throws IOException {
|
|
||||||
short result = 0;
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
result |= (in.readUnsignedByte() << (8 * i));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String readString(int length) throws IOException {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[length];
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
buffer[i] = in.readByte();
|
|
||||||
}
|
|
||||||
return new String(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] readBytes(int length) throws IOException {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[length];
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
buffer[i] = in.readByte();
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int read(byte[] b, int offset, int length) throws IOException {
|
|
||||||
return in.read(b, offset, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class ZipListingHelper {
|
|
||||||
|
|
||||||
static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yy HH:mm", Locale.ENGLISH);
|
|
||||||
|
|
||||||
public static void listHeader(LoggerInterface log) {
|
|
||||||
log.debug(" Length Method Size Ratio Date Time CRC-32 Name");
|
|
||||||
log.debug("-------- ------ ------- ----- ---- ---- ------ ----");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void listEntry(LoggerInterface log, ZioEntry entry) {
|
|
||||||
int ratio = 0;
|
|
||||||
if (entry.getSize() > 0) ratio = (100 * (entry.getSize() - entry.getCompressedSize())) / entry.getSize();
|
|
||||||
log.debug(String.format(Locale.ENGLISH, "%8d %6s %8d %4d%% %s %08x %s",
|
|
||||||
entry.getSize(),
|
|
||||||
entry.getCompression() == 0 ? "Stored" : "Defl:N",
|
|
||||||
entry.getCompressedSize(),
|
|
||||||
ratio,
|
|
||||||
dateFormat.format(new Date(entry.getTime())),
|
|
||||||
entry.getCrc32(),
|
|
||||||
entry.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 Ken Ellinwood
|
|
||||||
*
|
|
||||||
* 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 kellinwood.zipio;
|
|
||||||
|
|
||||||
import kellinwood.logging.LoggerInterface;
|
|
||||||
import kellinwood.logging.LoggerManager;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class ZipOutput {
|
|
||||||
|
|
||||||
static LoggerInterface log;
|
|
||||||
|
|
||||||
String outputFilename;
|
|
||||||
OutputStream out = null;
|
|
||||||
int filePointer = 0;
|
|
||||||
|
|
||||||
List<ZioEntry> entriesWritten = new LinkedList<ZioEntry>();
|
|
||||||
Set<String> namesWritten = new HashSet<String>();
|
|
||||||
|
|
||||||
public ZipOutput(String filename) throws IOException {
|
|
||||||
this.outputFilename = filename;
|
|
||||||
File ofile = new File(outputFilename);
|
|
||||||
init(ofile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZipOutput(File outputFile) throws IOException {
|
|
||||||
this.outputFilename = outputFile.getAbsolutePath();
|
|
||||||
File ofile = outputFile;
|
|
||||||
init(ofile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(File ofile) throws IOException {
|
|
||||||
if (ofile.exists()) ofile.delete();
|
|
||||||
out = new FileOutputStream(ofile);
|
|
||||||
if (getLogger().isDebugEnabled()) ZipListingHelper.listHeader(getLogger());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZipOutput(OutputStream os) throws IOException {
|
|
||||||
out = os;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoggerInterface getLogger() {
|
|
||||||
if (log == null) log = LoggerManager.getLogger(ZipOutput.class.getName());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(ZioEntry entry) throws IOException {
|
|
||||||
String entryName = entry.getName();
|
|
||||||
if (namesWritten.contains(entryName)) {
|
|
||||||
getLogger().warning("Skipping duplicate file in output: " + entryName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entry.writeLocalEntry(this);
|
|
||||||
entriesWritten.add(entry);
|
|
||||||
namesWritten.add(entryName);
|
|
||||||
if (getLogger().isDebugEnabled()) ZipListingHelper.listEntry(getLogger(), entry);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void close() throws IOException {
|
|
||||||
CentralEnd centralEnd = new CentralEnd();
|
|
||||||
|
|
||||||
centralEnd.centralStartOffset = (int) getFilePointer();
|
|
||||||
centralEnd.numCentralEntries = centralEnd.totalCentralEntries = (short) entriesWritten.size();
|
|
||||||
|
|
||||||
for (ZioEntry entry : entriesWritten) {
|
|
||||||
entry.write(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
centralEnd.centralDirectorySize = (int) (getFilePointer() - centralEnd.centralStartOffset);
|
|
||||||
centralEnd.fileComment = "";
|
|
||||||
|
|
||||||
centralEnd.write(this);
|
|
||||||
|
|
||||||
if (out != null) try {
|
|
||||||
out.close();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getFilePointer() throws IOException {
|
|
||||||
return filePointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void writeInt(int value) throws IOException {
|
|
||||||
byte[] data = new byte[4];
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
data[i] = (byte) (value & 0xFF);
|
|
||||||
value = value >> 8;
|
|
||||||
}
|
|
||||||
out.write(data);
|
|
||||||
filePointer += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeShort(short value) throws IOException {
|
|
||||||
byte[] data = new byte[2];
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
data[i] = (byte) (value & 0xFF);
|
|
||||||
value = (short) (value >> 8);
|
|
||||||
}
|
|
||||||
out.write(data);
|
|
||||||
filePointer += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeString(String value) throws IOException {
|
|
||||||
|
|
||||||
byte[] data = value.getBytes();
|
|
||||||
out.write(data);
|
|
||||||
filePointer += data.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeBytes(byte[] value) throws IOException {
|
|
||||||
|
|
||||||
out.write(value);
|
|
||||||
filePointer += value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeBytes(byte[] value, int offset, int length) throws IOException {
|
|
||||||
|
|
||||||
out.write(value, offset, length);
|
|
||||||
filePointer += length;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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