Compare commits

..

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

1831 changed files with 19145 additions and 124097 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

54
.gitignore vendored
View File

@ -1,49 +1,9 @@
# 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/
*.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)

83
AndroidManifest.xml Normal file
View File

@ -0,0 +1,83 @@
<?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="39"
android:versionName="0.39-test" >
<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>
<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

339
COPYING Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 2 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.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

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

674
LICENSE
View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

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,35 +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.content.Intent;
import androidx.annotation.Nullable;
/**
* Dummy version for basic app flavor.
*/
public class WifiStateChangeService {
public static void start(Context context, @Nullable Intent intent) {
}
public class WifiInfoThread extends Thread {
}
}

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

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