Compare commits

..

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

1886 changed files with 28674 additions and 123393 deletions

8
.android2po Normal file
View 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
View File

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

53
.gitignore vendored
View File

@ -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
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Editor swap/save files
build.properties
project.properties
.classpath
bin/*
gen/*
proguard.cfg
proguard-project.txt
*~
*.swp
# More IDE stuff
.idea/
.idea
*.iml
out
.settings/
# Imported libs
extern/*/libs/
extern/*/*/libs/
# Tests
junit-report.xml
# Screen dumps from Android Studio/DDMS
captures/
/fdroid/

View File

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

View File

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

View File

@ -1,26 +1,8 @@
LOCAL_PATH:= $(call my-dir)
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := F-Droid
LOCAL_MODULE_TAGS := optional
LOCAL_PACKAGE_NAME := F-Droid
LOCAL_PACKAGE_NAME := FDroid
LOCAL_SRC_FILES := $(call all-java-files-under,src)
fdroid_root := $(LOCAL_PATH)
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
include $(BUILD_PACKAGE)
$(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
View 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>

File diff suppressed because it is too large Load Diff

View File

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

View File

View File

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

View File

@ -1,67 +0,0 @@
# F-Droid Client
[![build status](https://gitlab.com/fdroid/fdroidclient/badges/master/pipeline.svg)](https://gitlab.com/fdroid/fdroidclient/-/jobs)
[![Translation status](https://hosted.weblate.org/widgets/f-droid/-/svg-badge.svg)](https://hosted.weblate.org/engage/f-droid/)
Client for [F-Droid](https://f-droid.org), the Free Software repository system
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.
[![translation status](https://hosted.weblate.org/widgets/f-droid/-/f-droid/multi-auto.svg)](https://hosted.weblate.org/engage/f-droid/?utm_source=widget)

18
ant.properties Normal file
View 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

View File

@ -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]) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,372 +0,0 @@
package org.fdroid.fdroid;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Replacer for the netstat utility, by reading the /proc filesystem it can find out the
* open connections of the system
* From http://www.ussg.iu.edu/hypermail/linux/kernel/0409.1/2166.html :
* It will first list all listening TCP sockets, and next list all established
* TCP connections. A typical entry of /proc/net/tcp would look like this (split
* up into 3 parts because of the length of the line):
* <p>
* 46: 010310AC:9C4C 030310AC:1770 01
* | | | | | |--> connection state
* | | | | |------> remote TCP port number
* | | | |-------------> remote IPv4 address
* | | |--------------------> local TCP port number
* | |---------------------------> local IPv4 address
* |----------------------------------> number of entry
* <p>
* 00000150:00000000 01:00000019 00000000
* | | | | |--> number of unrecovered RTO timeouts
* | | | |----------> number of jiffies until timer expires
* | | |----------------> timer_active (see below)
* | |----------------------> receive-queue
* |-------------------------------> transmit-queue
* <p>
* 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
* | | | | | | | | | |--> slow start size threshold,
* | | | | | | | | | or -1 if the treshold
* | | | | | | | | | is >= 0xFFFF
* | | | | | | | | |----> sending congestion window
* | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
* | | | | | | |---------> Predicted tick of soft clock
* | | | | | | (delayed ACK control data)
* | | | | | |------------> retransmit timeout
* | | | | |------------------> location of socket in memory
* | | | |-----------------------> socket reference count
* | | |-----------------------------> inode
* | |----------------------------------> unanswered 0-window probes
* |---------------------------------------------> uid
*
* @author Ciprian Dobre
*/
public class Netstat {
/**
* Possible values for states in /proc/net/tcp
*/
private static final String[] STATES = {
"ESTBLSH", "SYNSENT", "SYNRECV", "FWAIT1", "FWAIT2", "TMEWAIT",
"CLOSED", "CLSWAIT", "LASTACK", "LISTEN", "CLOSING", "UNKNOWN",
};
/**
* Pattern used when parsing through /proc/net/tcp
*/
private static final Pattern NET_PATTERN = Pattern.compile(
"\\d+:\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+)\\s+" +
"[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+\\s+([\\d]+)\\s+[\\d]+\\s+([\\d]+)");
/**
* Utility method that converts an address from a hex representation as founded in /proc to String representation
*/
private static String getAddress(final String hexa) {
try {
// first let's convert the address to Integer
final long v = Long.parseLong(hexa, 16);
// in /proc the order is little endian and java uses big endian order we also need to invert the order
final long adr = (v >>> 24) | (v << 24) |
((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
// and now it's time to output the result
return ((adr >> 24) & 0xff) + "." + ((adr >> 16) & 0xff) + "." + ((adr >> 8) & 0xff) + "." + (adr & 0xff);
} catch (Exception ex) {
ex.printStackTrace();
return "0.0.0.0"; // NOPMD
}
}
private static int getInt16(final String hexa) {
try {
return Integer.parseInt(hexa, 16);
} catch (Exception ex) {
ex.printStackTrace();
return -1;
}
}
/*
private static String getPName(final int pid) {
final Pattern pattern = Pattern.compile("Name:\\s*(\\S+)");
try {
BufferedReader in = new BufferedReader(new FileReader("/proc/" + pid + "/status"));
String line;
while ((line = in.readLine()) != null) {
final Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return matcher.group(1);
}
}
in.close();
} catch (Throwable t) {
// ignored
}
return "UNKNOWN";
}
*/
/**
* Method used to question for the connections currently openned
*
* @return The list of connections (as Connection objects)
*/
public static List<Connection> getConnections() {
final ArrayList<Connection> net = new ArrayList<>();
// read from /proc/net/tcp the list of currently openned socket connections
try {
BufferedReader in = new BufferedReader(new FileReader("/proc/net/tcp"));
String line;
while ((line = in.readLine()) != null) { // NOPMD
Matcher matcher = NET_PATTERN.matcher(line);
if (matcher.find()) {
final Connection c = new Connection();
c.setProtocol(Connection.TCP_CONNECTION);
net.add(c);
final String localPortHexa = matcher.group(2);
final String remoteAddressHexa = matcher.group(3);
final String remotePortHexa = matcher.group(4);
final String statusHexa = matcher.group(5);
//final String uid = matcher.group(6);
//final String inode = matcher.group(7);
c.setLocalPort(getInt16(localPortHexa));
c.setRemoteAddress(getAddress(remoteAddressHexa));
c.setRemotePort(getInt16(remotePortHexa));
try {
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
} catch (Exception ex) {
c.setStatus(STATES[11]); // unknown
}
c.setPID(-1); // unknown
c.setPName("UNKNOWN");
}
}
in.close();
} catch (Throwable t) { // NOPMD
// ignored
}
// read from /proc/net/udp the list of currently openned socket connections
try {
BufferedReader in = new BufferedReader(new FileReader("/proc/net/udp"));
String line;
while ((line = in.readLine()) != null) { // NOPMD
Matcher matcher = NET_PATTERN.matcher(line);
if (matcher.find()) {
final Connection c = new Connection();
c.setProtocol(Connection.UDP_CONNECTION);
net.add(c);
final String localPortHexa = matcher.group(2);
final String remoteAddressHexa = matcher.group(3);
final String remotePortHexa = matcher.group(4);
final String statusHexa = matcher.group(5);
//final String uid = matcher.group(6);
//final String inode = matcher.group(7);
c.setLocalPort(getInt16(localPortHexa));
c.setRemoteAddress(getAddress(remoteAddressHexa));
c.setRemotePort(getInt16(remotePortHexa));
try {
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
} catch (Exception ex) {
c.setStatus(STATES[11]); // unknown
}
c.setPID(-1); // unknown
c.setPName("UNKNOWN");
}
}
in.close();
} catch (Throwable t) { // NOPMD
// ignored
}
// read from /proc/net/raw the list of currently openned socket connections
try {
BufferedReader in = new BufferedReader(new FileReader("/proc/net/raw"));
String line;
while ((line = in.readLine()) != null) { // NOPMD
Matcher matcher = NET_PATTERN.matcher(line);
if (matcher.find()) {
final Connection c = new Connection();
c.setProtocol(Connection.RAW_CONNECTION);
net.add(c);
//final String localAddressHexa = matcher.group(1);
final String localPortHexa = matcher.group(2);
final String remoteAddressHexa = matcher.group(3);
final String remotePortHexa = matcher.group(4);
final String statusHexa = matcher.group(5);
//final String uid = matcher.group(6);
//final String inode = matcher.group(7);
c.setLocalPort(getInt16(localPortHexa));
c.setRemoteAddress(getAddress(remoteAddressHexa));
c.setRemotePort(getInt16(remotePortHexa));
try {
c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
} catch (Exception ex) {
c.setStatus(STATES[11]); // unknown
}
c.setPID(-1); // unknown
c.setPName("UNKNOWN");
}
}
in.close();
} catch (Throwable t) { // NOPMD
// ignored
}
return net;
}
/**
* Informations about a given connection
*
* @author Ciprian Dobre
*/
public static class Connection {
/**
* Types of connection protocol
***/
public static final byte TCP_CONNECTION = 0;
public static final byte UDP_CONNECTION = 1;
public static final byte RAW_CONNECTION = 2;
/**
* <code>serialVersionUID</code>
*/
private static final long serialVersionUID = 1988671591829311032L;
/**
* The protocol of the connection (can be tcp, udp or raw)
*/
protected byte protocol;
/**
* The owner of the connection (username)
*/
protected String powner;
/**
* The pid of the owner process
*/
protected int pid;
/**
* The name of the program owning the connection
*/
protected String pname;
/**
* Local port
*/
protected int localPort;
/**
* Remote address of the connection
*/
protected String remoteAddress;
/**
* Remote port
*/
protected int remotePort;
/**
* Status of the connection
*/
protected String status;
public final byte getProtocol() {
return protocol;
}
public final void setProtocol(final byte protocol) {
this.protocol = protocol;
}
public final String getProtocolAsString() {
switch (protocol) {
case TCP_CONNECTION:
return "TCP";
case UDP_CONNECTION:
return "UDP";
case RAW_CONNECTION:
return "RAW";
}
return "UNKNOWN";
}
public final String getPOwner() {
return powner;
}
public final void setPOwner(final String owner) {
this.powner = owner;
}
public final int getPID() {
return pid;
}
public final void setPID(final int pid) {
this.pid = pid;
}
public final String getPName() {
return pname;
}
public final void setPName(final String pname) {
this.pname = pname;
}
public final int getLocalPort() {
return localPort;
}
public final void setLocalPort(final int localPort) {
this.localPort = localPort;
}
public final String getRemoteAddress() {
return remoteAddress;
}
public final void setRemoteAddress(final String remoteAddress) {
this.remoteAddress = remoteAddress;
}
public final int getRemotePort() {
return remotePort;
}
public final void setRemotePort(final int remotePort) {
this.remotePort = remotePort;
}
public final String getStatus() {
return status;
}
public final void setStatus(final String status) {
this.status = status;
}
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("[Prot=").append(getProtocolAsString());
buf.append(",POwner=").append(powner);
buf.append(",PID=").append(pid);
buf.append(",PName=").append(pname);
buf.append(",LPort=").append(localPort);
buf.append(",RAddress=").append(remoteAddress);
buf.append(",RPort=").append(remotePort);
buf.append(",Status=").append(status);
buf.append("]");
return buf.toString();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,110 +0,0 @@
package org.fdroid.fdroid.work;
import android.app.Instrumentation;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.compat.FileCompatTest;
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* This test cannot run on Robolectric unfortunately since it does not support
* getting the timestamps from the files completely.
* <p>
* This is marked with {@link LargeTest} because it always fails on the emulator
* tests on GitLab CI. That excludes it from the test run there.
*/
@LargeTest
public class CleanCacheWorkerTest {
public static final String TAG = "CleanCacheWorkerEmulatorTest";
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
@Rule
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
@Test
public void testWorkRequest() throws ExecutionException, InterruptedException {
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CleanCacheWorker.class).build();
workManagerTestRule.workManager.enqueue(request).getResult();
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
}
@Test
public void testClearOldFiles() throws IOException, InterruptedException {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
File tempDir = FileCompatTest.getWriteableDir(instrumentation);
assertTrue(tempDir.isDirectory());
assertTrue(tempDir.canWrite());
File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
FileUtils.deleteQuietly(dir);
assertTrue(dir.mkdirs());
assertTrue(dir.isDirectory());
File first = new File(dir, "first");
first.deleteOnExit();
File second = new File(dir, "second");
second.deleteOnExit();
assertFalse(first.exists());
assertFalse(second.exists());
assertTrue(first.createNewFile());
assertTrue(first.exists());
Thread.sleep(7000);
assertTrue(second.createNewFile());
assertTrue(second.exists());
CleanCacheWorker.clearOldFiles(dir, 3000); // check all in dir
assertFalse(first.exists());
assertTrue(second.exists());
Thread.sleep(7000);
CleanCacheWorker.clearOldFiles(second, 3000); // check just second file
assertFalse(first.exists());
assertFalse(second.exists());
// make sure it doesn't freak out on a non-existent file
File nonexistent = new File(tempDir, "nonexistent");
CleanCacheWorker.clearOldFiles(nonexistent, 1);
CleanCacheWorker.clearOldFiles(null, 1);
}
/*
// TODO enable this once getImageCacheDir() can be mocked or provide a writable dir in the test
@Test
public void testDeleteOldIcons() throws IOException {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
File imageCacheDir = Utils.getImageCacheDir(context);
imageCacheDir.mkdirs();
assertTrue(imageCacheDir.isDirectory());
File oldIcon = new File(imageCacheDir, "old.png");
assertTrue(oldIcon.createNewFile());
Assume.assumeTrue("test environment must be able to set LastModified time",
oldIcon.setLastModified(System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 370)));
File currentIcon = new File(imageCacheDir, "current.png");
assertTrue(currentIcon.createNewFile());
CleanCacheWorker.deleteOldIcons(context);
assertTrue(currentIcon.exists());
assertFalse(oldIcon.exists());
}
*/
}

View File

@ -1,72 +0,0 @@
/*
* Copyright (C) 2021 Hans-Christoph Steiner <hans@eds.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.fdroid.fdroid.work;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import static org.junit.Assert.assertEquals;
/**
* This actually runs {@link FDroidMetricsWorker} on a device/emulator and
* submits a report to https://metrics.cleaninsights.org
* <p>
* This is marked with {@link LargeTest} to exclude it from running on GitLab CI
* because it always fails on the emulator tests there. Also, it actually submits
* a report.
*/
@LargeTest
public class FDroidMetricsWorkerTest {
public static final String TAG = "FDroidMetricsWorkerTest";
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
@Rule
public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
/**
* A test for easy manual testing.
*/
@Ignore
@Test
public void testGenerateReport() throws IOException {
String json = FDroidMetricsWorker.generateReport(
InstrumentationRegistry.getInstrumentation().getTargetContext());
System.out.println(json);
}
@Test
public void testWorkRequest() throws ExecutionException, InterruptedException {
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(FDroidMetricsWorker.class).build();
workManagerTestRule.workManager.enqueue(request).getResult();
ListenableFuture<WorkInfo> workInfo = workManagerTestRule.workManager.getWorkInfoById(request.getId());
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.get().getState());
}
}

View File

@ -1,33 +0,0 @@
package org.fdroid.fdroid.work;
import android.app.Instrumentation;
import android.content.Context;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.work.Configuration;
import androidx.work.WorkManager;
import androidx.work.testing.SynchronousExecutor;
import androidx.work.testing.WorkManagerTestInitHelper;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
public class WorkManagerTestRule extends TestWatcher {
Context targetContext;
Context testContext;
Configuration configuration;
WorkManager workManager;
@Override
protected void starting(Description description) {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
targetContext = instrumentation.getTargetContext();
testContext = instrumentation.getContext();
configuration = new Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(new SynchronousExecutor())
.build();
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration);
workManager = WorkManager.getInstance(targetContext);
}
}

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2018 Hans-Christoph Steiner <hans@eds.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
import android.content.Context;
/**
* Dummy version for basic app flavor.
*/
public class SDCardScannerService {
public static void scan(Context context) {
}
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2018 Hans-Christoph Steiner <hans@eds.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
import android.content.Context;
/**
* Dummy version for basic app flavor.
*/
public class SwapService {
public static void start(Context context) {
}
}

View File

@ -1,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) {
}
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2018 Senecto Limited
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.nearby;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
/**
* Dummy version for basic app flavor.
*/
public class TreeUriScannerIntentService {
public static void onActivityResult(AppCompatActivity activity, Intent intent) {
throw new IllegalStateException("unimplemented");
}
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
package kellinwood.security.zipsigner.optional;
public class KeyNameConflictException extends Exception {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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